389 lines
15 KiB
Python
389 lines
15 KiB
Python
# 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 copy
|
|
import datetime
|
|
import functools
|
|
import uuid
|
|
|
|
from oslo_serialization import jsonutils
|
|
from oslo_utils import timeutils
|
|
import swiftclient
|
|
|
|
from zaqar.common import decorators
|
|
from zaqar import storage
|
|
from zaqar.storage import errors
|
|
from zaqar.storage.swift import utils
|
|
from zaqar.storage import utils as s_utils
|
|
|
|
|
|
class MessageController(storage.Message):
|
|
"""Implements message resource operations with swift backend
|
|
|
|
Messages are scoped by project + queue.
|
|
|
|
message -> Swift mapping:
|
|
+--------------+-----------------------------------------+
|
|
| Attribute | Storage location |
|
|
+--------------+-----------------------------------------+
|
|
| Msg UUID | Object name |
|
|
+--------------+-----------------------------------------+
|
|
| Queue Name | Container name prefix |
|
|
+--------------+-----------------------------------------+
|
|
| Project name | Container name prefix |
|
|
+--------------+-----------------------------------------+
|
|
| Created time | Object Creation Time |
|
|
+--------------+-----------------------------------------+
|
|
| Msg Body | Object content 'body' |
|
|
+--------------+-----------------------------------------+
|
|
| Client ID | Object header 'ClientID' |
|
|
+--------------+-----------------------------------------+
|
|
| Claim ID | Object content 'claim_id' |
|
|
+--------------+-----------------------------------------+
|
|
| Delay Expires| Object content 'delay_expires' |
|
|
+--------------+-----------------------------------------+
|
|
| Expires | Object Delete-After header |
|
|
+--------------------------------------------------------+
|
|
| Checksum | Object content 'body' checksum |
|
|
+--------------------------------------------------------+
|
|
"""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(MessageController, self).__init__(*args, **kwargs)
|
|
self._client = self.driver.connection
|
|
|
|
@decorators.lazy_property(write=False)
|
|
def _queue_ctrl(self):
|
|
return self.driver.queue_controller
|
|
|
|
def _delete_queue_messages(self, queue, project, pipe):
|
|
"""Method to remove all the messages belonging to a queue.
|
|
|
|
Will be referenced from the QueueController.
|
|
The pipe to execute deletion will be passed from the QueueController
|
|
executing the operation.
|
|
"""
|
|
container = utils._message_container(queue, project)
|
|
remaining = True
|
|
key = ''
|
|
while remaining:
|
|
headers, objects = self._client.get_container(container,
|
|
limit=1000,
|
|
marker=key)
|
|
if not objects:
|
|
return
|
|
remaining = len(objects) == 1000
|
|
key = objects[-1]['name']
|
|
for o in objects:
|
|
try:
|
|
self._client.delete_object(container, o['name'])
|
|
except swiftclient.ClientException as exc:
|
|
if exc.http_status == 404:
|
|
continue
|
|
raise
|
|
|
|
def _list(self, queue, project=None, marker=None,
|
|
limit=storage.DEFAULT_MESSAGES_PER_PAGE,
|
|
echo=False, client_uuid=None,
|
|
include_claimed=False, include_delayed=False,
|
|
sort=1):
|
|
"""List messages in the queue, oldest first(ish)
|
|
|
|
Time ordering and message inclusion in lists are soft, there is no
|
|
global order and times are based on the UTC time of the zaqar-api
|
|
server that the message was created from.
|
|
|
|
Here be consistency dragons.
|
|
"""
|
|
if not self._queue_ctrl.exists(queue, project):
|
|
raise errors.QueueDoesNotExist(queue, project)
|
|
|
|
client = self._client
|
|
container = utils._message_container(queue, project)
|
|
query_string = None
|
|
if sort == -1:
|
|
query_string = 'reverse=on'
|
|
|
|
try:
|
|
_, objects = client.get_container(
|
|
container,
|
|
marker=marker,
|
|
# list 2x the objects because some listing items may have
|
|
# expired
|
|
limit=limit * 2,
|
|
query_string=query_string)
|
|
except swiftclient.ClientException as exc:
|
|
if exc.http_status == 404:
|
|
raise errors.QueueDoesNotExist(queue, project)
|
|
raise
|
|
|
|
def is_claimed(msg, headers):
|
|
if include_claimed or msg['claim_id'] is None:
|
|
return False
|
|
claim_obj = self.driver.claim_controller._get(
|
|
queue, msg['claim_id'], project)
|
|
return claim_obj is not None and claim_obj['ttl'] > 0
|
|
|
|
def is_delayed(msg, headers):
|
|
if include_delayed:
|
|
return False
|
|
now = timeutils.utcnow_ts()
|
|
return msg.get('delay_expires', 0) > now
|
|
|
|
def is_echo(msg, headers):
|
|
if echo:
|
|
return False
|
|
return headers['x-object-meta-clientid'] == str(client_uuid)
|
|
|
|
filters = [
|
|
is_echo,
|
|
is_claimed,
|
|
is_delayed,
|
|
]
|
|
marker = {}
|
|
get_object = functools.partial(client.get_object, container)
|
|
list_objects = functools.partial(client.get_container, container,
|
|
limit=limit * 2,
|
|
query_string=query_string)
|
|
yield utils._filter_messages(objects, filters, marker, get_object,
|
|
list_objects, limit=limit)
|
|
yield marker and marker['next']
|
|
|
|
def list(self, queue, project=None, marker=None,
|
|
limit=storage.DEFAULT_MESSAGES_PER_PAGE,
|
|
echo=False, client_uuid=None,
|
|
include_claimed=False, include_delayed=False,):
|
|
return self._list(queue, project, marker, limit, echo,
|
|
client_uuid, include_claimed, include_delayed)
|
|
|
|
def first(self, queue, project=None, sort=1):
|
|
if sort not in (1, -1):
|
|
raise ValueError(u'sort must be either 1 (ascending) '
|
|
u'or -1 (descending)')
|
|
cursor = self._list(queue, project, limit=1, sort=sort)
|
|
try:
|
|
message = next(next(cursor))
|
|
except StopIteration:
|
|
raise errors.QueueIsEmpty(queue, project)
|
|
return message
|
|
|
|
def get(self, queue, message_id, project=None):
|
|
return self._get(queue, message_id, project)
|
|
|
|
def _get(self, queue, message_id, project=None, check_queue=True):
|
|
if check_queue and not self._queue_ctrl.exists(queue, project):
|
|
raise errors.QueueDoesNotExist(queue, project)
|
|
|
|
now = timeutils.utcnow_ts(True)
|
|
|
|
headers, msg = self._find_message(queue, message_id, project)
|
|
return utils._message_to_json(message_id, msg, headers, now)
|
|
|
|
def _find_message(self, queue, message_id, project):
|
|
try:
|
|
return self._client.get_object(
|
|
utils._message_container(queue, project), message_id)
|
|
|
|
except swiftclient.ClientException as exc:
|
|
if exc.http_status == 404:
|
|
raise errors.MessageDoesNotExist(message_id, queue, project)
|
|
else:
|
|
raise
|
|
|
|
def bulk_delete(self, queue, message_ids, project=None, claim_ids=None):
|
|
for message_id in message_ids:
|
|
try:
|
|
if claim_ids:
|
|
msg = self._get(queue, message_id, project)
|
|
if not msg['claim_id']:
|
|
raise errors.MessageNotClaimed(message_id)
|
|
if msg['claim_id'] not in claim_ids:
|
|
raise errors.ClaimDoesNotMatch(msg['claim_id'],
|
|
queue, project)
|
|
self._delete(queue, message_id, project)
|
|
except errors.MessageDoesNotExist:
|
|
pass
|
|
|
|
def bulk_get(self, queue, message_ids, project=None):
|
|
if not self._queue_ctrl.exists(queue, project):
|
|
raise StopIteration()
|
|
|
|
for id in message_ids:
|
|
try:
|
|
yield self._get(queue, id, project, check_queue=False)
|
|
except errors.MessageDoesNotExist:
|
|
pass
|
|
|
|
def post(self, queue, messages, client_uuid, project=None):
|
|
# TODO(flwang): It would be nice if we can create a middleware in Swift
|
|
# to accept a json list so that Zaqar can create objects in bulk.
|
|
return [self._create_msg(queue, m, client_uuid, project)
|
|
for m in messages]
|
|
|
|
def _create_msg(self, queue, msg, client_uuid, project):
|
|
slug = str(uuid.uuid1())
|
|
now = timeutils.utcnow_ts()
|
|
message = {'body': msg.get('body', {}), 'claim_id': None,
|
|
'ttl': msg['ttl'], 'claim_count': 0,
|
|
'delay_expires': now + msg.get('delay', 0)}
|
|
|
|
if self.driver.conf.enable_checksum:
|
|
message['checksum'] = s_utils.get_checksum(msg.get('body', None))
|
|
|
|
contents = jsonutils.dumps(message)
|
|
utils._put_or_create_container(
|
|
self._client,
|
|
utils._message_container(queue, project),
|
|
slug,
|
|
contents=contents,
|
|
content_type='application/json',
|
|
headers={
|
|
'x-object-meta-clientid': str(client_uuid),
|
|
'x-delete-after': msg['ttl']})
|
|
return slug
|
|
|
|
def delete(self, queue, message_id, project=None, claim=None):
|
|
claim_ctrl = self.driver.claim_controller
|
|
try:
|
|
msg = self._get(queue, message_id, project)
|
|
except (errors.QueueDoesNotExist, errors.MessageDoesNotExist):
|
|
return
|
|
if claim is None:
|
|
if msg['claim_id']:
|
|
claim_obj = claim_ctrl._get(queue, msg['claim_id'], project)
|
|
if claim_obj is not None and claim_obj['ttl'] > 0:
|
|
raise errors.MessageIsClaimed(message_id)
|
|
else:
|
|
# Check if the claim does exist
|
|
claim_ctrl._exists(queue, claim, project)
|
|
if not msg['claim_id']:
|
|
raise errors.MessageNotClaimed(message_id)
|
|
elif msg['claim_id'] != claim:
|
|
raise errors.MessageNotClaimedBy(message_id, claim)
|
|
|
|
self._delete(queue, message_id, project)
|
|
|
|
def _delete(self, queue, message_id, project=None):
|
|
try:
|
|
self._client.delete_object(
|
|
utils._message_container(queue, project), message_id)
|
|
except swiftclient.ClientException as exc:
|
|
if exc.http_status != 404:
|
|
raise
|
|
|
|
def pop(self, queue, limit, project=None):
|
|
# Pop is implemented as a chain of the following operations:
|
|
# 1. Create a claim.
|
|
# 2. Delete the messages claimed.
|
|
# 3. Delete the claim.
|
|
claim_ctrl = self.driver.claim_controller
|
|
claim_id, messages = claim_ctrl.create(queue, dict(ttl=1, grace=0),
|
|
project, limit=limit)
|
|
|
|
message_ids = [message['id'] for message in messages]
|
|
self.bulk_delete(queue, message_ids, project)
|
|
return messages
|
|
|
|
|
|
class MessageQueueHandler(object):
|
|
def __init__(self, driver, control_driver):
|
|
self.driver = driver
|
|
self._client = self.driver.connection
|
|
self._queue_ctrl = self.driver.queue_controller
|
|
self._message_ctrl = self.driver.message_controller
|
|
self._claim_ctrl = self.driver.claim_controller
|
|
|
|
def create(self, name, metadata=None, project=None):
|
|
self._client.put_container(utils._message_container(name, project))
|
|
|
|
def delete(self, name, project=None):
|
|
for container in [utils._message_container(name, project),
|
|
utils._claim_container(name, project)]:
|
|
try:
|
|
headers, objects = self._client.get_container(container)
|
|
except swiftclient.ClientException as exc:
|
|
if exc.http_status != 404:
|
|
raise
|
|
else:
|
|
for obj in objects:
|
|
try:
|
|
self._client.delete_object(container, obj['name'])
|
|
except swiftclient.ClientException as exc:
|
|
if exc.http_status != 404:
|
|
raise
|
|
try:
|
|
self._client.delete_container(container)
|
|
except swiftclient.ClientException as exc:
|
|
if exc.http_status not in (404, 409):
|
|
raise
|
|
|
|
def stats(self, name, project=None):
|
|
if not self._queue_ctrl.exists(name, project=project):
|
|
raise errors.QueueDoesNotExist(name, project)
|
|
|
|
total = 0
|
|
claimed = 0
|
|
container = utils._message_container(name, project)
|
|
|
|
try:
|
|
_, objects = self._client.get_container(container)
|
|
except swiftclient.ClientException as exc:
|
|
if exc.http_status == 404:
|
|
raise errors.QueueIsEmpty(name, project)
|
|
|
|
newest = None
|
|
oldest = None
|
|
now = timeutils.utcnow_ts(True)
|
|
for obj in objects:
|
|
try:
|
|
headers = self._client.head_object(container, obj['name'])
|
|
except swiftclient.ClientException as exc:
|
|
if exc.http_status != 404:
|
|
raise
|
|
else:
|
|
created = float(headers['x-timestamp'])
|
|
created_iso = datetime.datetime.utcfromtimestamp(
|
|
created).strftime('%Y-%m-%dT%H:%M:%SZ')
|
|
newest = {
|
|
'id': obj['name'],
|
|
'age': now - created,
|
|
'created': created_iso}
|
|
if oldest is None:
|
|
oldest = copy.deepcopy(newest)
|
|
total += 1
|
|
if headers.get('x-object-meta-claimid'):
|
|
claimed += 1
|
|
|
|
msg_stats = {
|
|
'claimed': claimed,
|
|
'free': total - claimed,
|
|
'total': total,
|
|
}
|
|
if newest is not None:
|
|
msg_stats['newest'] = newest
|
|
msg_stats['oldest'] = oldest
|
|
|
|
return {'messages': msg_stats}
|
|
|
|
def exists(self, queue, project=None):
|
|
try:
|
|
self._client.head_container(utils._message_container(queue,
|
|
project))
|
|
|
|
except swiftclient.ClientException as exc:
|
|
if exc.http_status == 404:
|
|
return False
|
|
raise
|
|
else:
|
|
return True
|