zaqar/zaqar/storage/mongodb/subscriptions.py

200 lines
7.3 KiB
Python

# Copyright (c) 2014 Catalyst IT Ltd.
#
# 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 datetime
from oslo_utils import timeutils
import pymongo.errors
from zaqar.common import utils as common_utils
from zaqar import storage
from zaqar.storage import base
from zaqar.storage import errors
from zaqar.storage.mongodb import utils
ID_INDEX_FIELDS = [('_id', 1)]
SUBSCRIPTIONS_INDEX = [
('s', 1),
('u', 1),
('p', 1),
]
# For removing expired subscriptions
TTL_INDEX_FIELDS = [
('e', 1),
]
class SubscriptionController(base.Subscription):
"""Implements subscription resource operations using MongoDB.
Subscriptions are unique by project + queue/topic + subscriber.
Schema:
's': source :: str
'u': subscriber:: str
't': ttl:: int
'e': expires: datetime.datetime
'o': options :: dict
'p': project :: str
'c': confirmed :: boolean
"""
def __init__(self, *args, **kwargs):
super(SubscriptionController, self).__init__(*args, **kwargs)
self._collection = self.driver.subscriptions_database.subscriptions
self._collection.create_index(SUBSCRIPTIONS_INDEX, unique=True)
# NOTE(flwang): MongoDB will automatically delete the subscription
# from the subscriptions collection when the subscription's 'e' value
# is older than the number of seconds specified in expireAfterSeconds,
# i.e. 0 seconds older in this case. As such, the data expires at the
# specified 'e' value.
self._collection.create_index(TTL_INDEX_FIELDS, name='ttl',
expireAfterSeconds=0,
background=True)
@utils.raises_conn_error
def list(self, queue, project=None, marker=None,
limit=storage.DEFAULT_SUBSCRIPTIONS_PER_PAGE):
query = {'s': queue, 'p': project}
if marker is not None:
query['_id'] = {'$gt': utils.to_oid(marker)}
projection = {'s': 1, 'u': 1, 't': 1, 'p': 1, 'o': 1, '_id': 1, 'c': 1}
cursor = self._collection.find(query, projection=projection)
cursor = cursor.limit(limit).sort('_id')
marker_name = {}
ntotal = self._collection.count_documents(query, limit=limit)
now = timeutils.utcnow_ts()
def normalizer(record):
marker_name['next'] = record['_id']
return _basic_subscription(record, now)
yield utils.HookedCursor(cursor, normalizer, ntotal=ntotal)
yield marker_name and marker_name['next']
@utils.raises_conn_error
def get(self, queue, subscription_id, project=None):
res = self._collection.find_one({'_id': utils.to_oid(subscription_id),
'p': project,
's': queue})
if not res:
raise errors.SubscriptionDoesNotExist(subscription_id)
now = timeutils.utcnow_ts()
return _basic_subscription(res, now)
@utils.raises_conn_error
def create(self, queue, subscriber, ttl, options, project=None):
source = queue
now = timeutils.utcnow_ts()
now_dt = datetime.datetime.utcfromtimestamp(now)
expires = now_dt + datetime.timedelta(seconds=ttl)
confirmed = False
try:
res = self._collection.insert_one({'s': source,
'u': subscriber,
't': ttl,
'e': expires,
'o': options,
'p': project,
'c': confirmed})
return res.inserted_id
except pymongo.errors.DuplicateKeyError:
return None
@utils.raises_conn_error
def exists(self, queue, subscription_id, project=None):
return self._collection.find_one({'_id': utils.to_oid(subscription_id),
'p': project}) is not None
@utils.raises_conn_error
def update(self, queue, subscription_id, project=None, **kwargs):
names = ('subscriber', 'ttl', 'options')
key_transform = lambda x: 'u' if x == 'subscriber' else x[0]
fields = common_utils.fields(kwargs, names,
pred=lambda x: x is not None,
key_transform=key_transform)
assert fields, ('`subscriber`, `ttl`, '
'or `options` not found in kwargs')
new_ttl = fields.get('t')
if new_ttl is not None:
now = timeutils.utcnow_ts()
now_dt = datetime.datetime.utcfromtimestamp(now)
expires = now_dt + datetime.timedelta(seconds=new_ttl)
fields['e'] = expires
try:
res = self._collection.update_one(
{'_id': utils.to_oid(subscription_id),
'p': project,
's': queue},
{'$set': fields},
upsert=False)
except pymongo.errors.DuplicateKeyError:
raise errors.SubscriptionAlreadyExists()
if res.matched_count == 0:
raise errors.SubscriptionDoesNotExist(subscription_id)
@utils.raises_conn_error
def delete(self, queue, subscription_id, project=None):
self._collection.delete_one({'_id': utils.to_oid(subscription_id),
'p': project,
's': queue})
@utils.raises_conn_error
def get_with_subscriber(self, queue, subscriber, project=None):
res = self._collection.find_one({'u': subscriber,
's': queue,
'p': project})
now = timeutils.utcnow_ts()
return _basic_subscription(res, now)
@utils.raises_conn_error
def confirm(self, queue, subscription_id, project=None, confirmed=True):
res = self._collection.update_one(
{'_id': utils.to_oid(subscription_id),
'p': project},
{'$set': {'c': confirmed}},
upsert=False)
if res.matched_count == 0:
raise errors.SubscriptionDoesNotExist(subscription_id)
def _basic_subscription(record, now):
# NOTE(Eva-i): unused here record's field 'e' (expires) has changed it's
# format from int (timestamp) to datetime since patch
# 1d122b1671792aff0055ed5396111cd441fb8269. Any future change about
# starting using 'e' field should make sure support both of the formats.
oid = record['_id']
age = now - utils.oid_ts(oid)
confirmed = record.get('c', True)
return {
'id': str(oid),
'source': record['s'],
'subscriber': record['u'],
'ttl': record['t'],
'age': int(age),
'options': record['o'],
'confirmed': confirmed,
}