Merge "Support dead letter queue for MongoDB"
This commit is contained in:
commit
9b966b4912
@ -133,6 +133,28 @@ pop:
|
|||||||
|
|
||||||
#### variables in request ####################################################
|
#### variables in request ####################################################
|
||||||
|
|
||||||
|
_dead_letter_queue:
|
||||||
|
type: string
|
||||||
|
in: body
|
||||||
|
required: False
|
||||||
|
description: |
|
||||||
|
The target the message will be moved to when the message can't processed
|
||||||
|
successfully after meet the max claim count. It's not supported to add
|
||||||
|
queue C as the dead letter queue for queue B where queue B has been set
|
||||||
|
as a dead letter queue for queue A. There is no default value for this
|
||||||
|
attribute. If it's not set explicitly, then that means there is no dead
|
||||||
|
letter queue for current queue. It is one of the ``reserved attributes``
|
||||||
|
of Zaqar queues.
|
||||||
|
|
||||||
|
_dead_letter_queue_messages_ttl:
|
||||||
|
type: integer
|
||||||
|
in: body
|
||||||
|
required: False
|
||||||
|
description: |
|
||||||
|
The new TTL setting for messages when moved to dead letter queue. If it's
|
||||||
|
not set, current TTL will be kept. It is one of the ``reserved attributes``
|
||||||
|
of Zaqar queues.
|
||||||
|
|
||||||
_default_message_ttl:
|
_default_message_ttl:
|
||||||
type: integer
|
type: integer
|
||||||
in: body
|
in: body
|
||||||
@ -145,6 +167,26 @@ _default_message_ttl:
|
|||||||
one of the ``reserved attributes`` of Zaqar queues. The value will be
|
one of the ``reserved attributes`` of Zaqar queues. The value will be
|
||||||
reverted to the default value after deleting it explicitly.
|
reverted to the default value after deleting it explicitly.
|
||||||
|
|
||||||
|
_flavor:
|
||||||
|
type: string
|
||||||
|
in: body
|
||||||
|
required: False
|
||||||
|
description: |
|
||||||
|
The flavor name which can tell Zaqar which storage pool will be used to
|
||||||
|
create the queue. It is one of the ``reserved attributes`` of Zaqar
|
||||||
|
queues.
|
||||||
|
|
||||||
|
_max_claim_count:
|
||||||
|
type: integer
|
||||||
|
in: body
|
||||||
|
required: False
|
||||||
|
description: |
|
||||||
|
The max number the message can be claimed. Generally,
|
||||||
|
it means the message cannot be processed successfully. There is no default
|
||||||
|
value for this attribute. If it's not set, then that means this feature
|
||||||
|
won't be enabled for current queue. It is one of the
|
||||||
|
``reserved attributes`` of Zaqar queues.
|
||||||
|
|
||||||
_max_messages_post_size:
|
_max_messages_post_size:
|
||||||
type: integer
|
type: integer
|
||||||
in: body
|
in: body
|
||||||
|
@ -72,6 +72,10 @@ The body of the request is empty.
|
|||||||
exceed 64 bytes in length, and it is limited to US-ASCII letters, digits,
|
exceed 64 bytes in length, and it is limited to US-ASCII letters, digits,
|
||||||
underscores, and hyphens.
|
underscores, and hyphens.
|
||||||
|
|
||||||
|
When create queue, user can specify metadata for the queue. Currently, Zaqar
|
||||||
|
supports below metadata: _flavor, _max_claim_count, _dead_letter_queue and
|
||||||
|
_dead_letter_queue_messages_ttl.
|
||||||
|
|
||||||
|
|
||||||
Normal response codes: 201, 204
|
Normal response codes: 201, 204
|
||||||
|
|
||||||
@ -88,6 +92,12 @@ Request Parameters
|
|||||||
.. rest_parameters:: parameters.yaml
|
.. rest_parameters:: parameters.yaml
|
||||||
|
|
||||||
- queue_name: queue_name
|
- queue_name: queue_name
|
||||||
|
- _dead_letter_queue: _dead_letter_queue
|
||||||
|
- _dead_letter_queue_messages_ttl: _dead_letter_queue_messages_ttl
|
||||||
|
- _default_message_ttl: _default_message_ttl
|
||||||
|
- _flavor: _flavor
|
||||||
|
- _max_claim_count: _max_claim_count
|
||||||
|
- _max_messages_post_size: _max_messages_post_size
|
||||||
|
|
||||||
Request Example
|
Request Example
|
||||||
---------------
|
---------------
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Support for dead letter queue is added for MongoDB. With this feature,
|
||||||
|
message will be moved to the specified dead letter queue if it's claimed
|
||||||
|
many times but still can't successfully processed by a client. New reseved
|
||||||
|
metadata keys of queue are added: _max_claim_count, _dead_letter_queue and
|
||||||
|
_dead_letter_queue_messages_ttl.
|
@ -24,12 +24,16 @@ Field Mappings:
|
|||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from bson import objectid
|
from bson import objectid
|
||||||
|
from oslo_log import log as logging
|
||||||
from oslo_utils import timeutils
|
from oslo_utils import timeutils
|
||||||
|
from pymongo.collection import ReturnDocument
|
||||||
|
|
||||||
from zaqar import storage
|
from zaqar import storage
|
||||||
from zaqar.storage import errors
|
from zaqar.storage import errors
|
||||||
from zaqar.storage.mongodb import utils
|
from zaqar.storage.mongodb import utils
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _messages_iter(msg_iter):
|
def _messages_iter(msg_iter):
|
||||||
"""Used to iterate through messages."""
|
"""Used to iterate through messages."""
|
||||||
@ -125,6 +129,7 @@ class ClaimController(storage.Claim):
|
|||||||
time being, to execute an update on a limited number of records.
|
time being, to execute an update on a limited number of records.
|
||||||
"""
|
"""
|
||||||
msg_ctrl = self.driver.message_controller
|
msg_ctrl = self.driver.message_controller
|
||||||
|
queue_ctrl = self.driver.queue_controller
|
||||||
|
|
||||||
ttl = metadata['ttl']
|
ttl = metadata['ttl']
|
||||||
grace = metadata['grace']
|
grace = metadata['grace']
|
||||||
@ -142,19 +147,25 @@ class ClaimController(storage.Claim):
|
|||||||
'id': oid,
|
'id': oid,
|
||||||
't': ttl,
|
't': ttl,
|
||||||
'e': claim_expires,
|
'e': claim_expires,
|
||||||
|
'c': 0 # NOTE(flwang): A placeholder which will be updated later
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get a list of active, not claimed nor expired
|
# Get a list of active, not claimed nor expired
|
||||||
# messages that could be claimed.
|
# messages that could be claimed.
|
||||||
msgs = msg_ctrl._active(queue, projection={'_id': 1}, project=project,
|
msgs = msg_ctrl._active(queue, projection={'_id': 1, 'c': 1},
|
||||||
|
project=project,
|
||||||
limit=limit)
|
limit=limit)
|
||||||
|
|
||||||
messages = iter([])
|
messages = iter([])
|
||||||
ids = [msg['_id'] for msg in msgs]
|
be_claimed = [(msg['_id'], msg['c'].get('c', 0)) for msg in msgs]
|
||||||
|
ids = [_id for _id, _ in be_claimed]
|
||||||
|
|
||||||
if len(ids) == 0:
|
if len(ids) == 0:
|
||||||
return None, messages
|
return None, messages
|
||||||
|
|
||||||
|
# Get the maxClaimCount and deadLetterQueue from current queue's meta
|
||||||
|
queue_meta = queue_ctrl.get(queue, project=project)
|
||||||
|
|
||||||
now = timeutils.utcnow_ts()
|
now = timeutils.utcnow_ts()
|
||||||
|
|
||||||
# NOTE(kgriffs): Set the claim field for
|
# NOTE(kgriffs): Set the claim field for
|
||||||
@ -186,6 +197,68 @@ class ClaimController(storage.Claim):
|
|||||||
{'$set': new_values},
|
{'$set': new_values},
|
||||||
upsert=False, multi=True)
|
upsert=False, multi=True)
|
||||||
|
|
||||||
|
if ('_max_claim_count' in queue_meta and
|
||||||
|
'_dead_letter_queue' in queue_meta):
|
||||||
|
LOG.debug(u"The list of messages being claimed: %(be_claimed)s",
|
||||||
|
{"be_claimed": be_claimed})
|
||||||
|
|
||||||
|
for _id, claimed_count in be_claimed:
|
||||||
|
# NOTE(flwang): We have claimed the message above, but we will
|
||||||
|
# update the claim count below. So that means, when the
|
||||||
|
# claimed_count equals queue_meta['_max_claim_count'], the
|
||||||
|
# message has met the threshold. And Zaqar will move it to the
|
||||||
|
# DLQ.
|
||||||
|
if claimed_count < queue_meta['_max_claim_count']:
|
||||||
|
# 1. Save the new max claim count for message
|
||||||
|
collection.update({'_id': _id,
|
||||||
|
'c.id': oid},
|
||||||
|
{'$set': {'c.c': claimed_count + 1}},
|
||||||
|
upsert=False)
|
||||||
|
LOG.debug(u"Message %(id)s has been claimed %(count)d "
|
||||||
|
u"times.", {"id": str(_id),
|
||||||
|
"count": claimed_count + 1})
|
||||||
|
else:
|
||||||
|
# 2. Check if the message's claim count has exceeded the
|
||||||
|
# max claim count defined in the queue, if so, move the
|
||||||
|
# message to the dead letter queue.
|
||||||
|
|
||||||
|
# NOTE(flwang): We're moving message directly. That means,
|
||||||
|
# the queue and dead letter queue must be created on the
|
||||||
|
# same storage pool. It's a technical tradeoff, because if
|
||||||
|
# we re-send the message to the dead letter queue by
|
||||||
|
# message controller, then we will lost all the claim
|
||||||
|
# information.
|
||||||
|
dlq_name = queue_meta['_dead_letter_queue']
|
||||||
|
new_msg = {'c.c': claimed_count,
|
||||||
|
'p_q': utils.scope_queue_name(dlq_name,
|
||||||
|
project)}
|
||||||
|
dlq_ttl = queue_meta.get("_dead_letter_queue_messages_ttl")
|
||||||
|
if dlq_ttl:
|
||||||
|
new_msg['t'] = dlq_ttl
|
||||||
|
kwargs = {"return_document": ReturnDocument.AFTER}
|
||||||
|
msg = collection.find_one_and_update({'_id': _id,
|
||||||
|
'c.id': oid},
|
||||||
|
{'$set': new_msg},
|
||||||
|
**kwargs)
|
||||||
|
dlq_collection = msg_ctrl._collection(dlq_name, project)
|
||||||
|
if not dlq_collection:
|
||||||
|
LOG.warning(u"Failed to find the message collection "
|
||||||
|
u"for queue %(dlq_name)s", {"dlq_name":
|
||||||
|
dlq_name})
|
||||||
|
return None, iter([])
|
||||||
|
result = dlq_collection.insert_one(msg)
|
||||||
|
if result.inserted_id:
|
||||||
|
collection.delete_one({'_id': _id})
|
||||||
|
LOG.debug(u"Message %(id)s has met the max claim count "
|
||||||
|
u"%(count)d, now it has been moved to dead "
|
||||||
|
u"letter queue %(dlq_name)s.",
|
||||||
|
{"id": str(_id), "count": claimed_count,
|
||||||
|
"dlq_name": dlq_name})
|
||||||
|
# NOTE(flwang): Because the claimed count has meet the
|
||||||
|
# max, so the current claim is not valid. And technically,
|
||||||
|
# it's failed to create the claim.
|
||||||
|
return None, iter([])
|
||||||
|
|
||||||
if updated != 0:
|
if updated != 0:
|
||||||
# NOTE(kgriffs): This extra step is necessary because
|
# NOTE(kgriffs): This extra step is necessary because
|
||||||
# in between having gotten a list of active messages
|
# in between having gotten a list of active messages
|
||||||
|
@ -637,7 +637,7 @@ class MessageController(storage.Message):
|
|||||||
't': message['ttl'],
|
't': message['ttl'],
|
||||||
'e': now_dt + datetime.timedelta(seconds=message['ttl']),
|
'e': now_dt + datetime.timedelta(seconds=message['ttl']),
|
||||||
'u': client_uuid,
|
'u': client_uuid,
|
||||||
'c': {'id': None, 'e': now},
|
'c': {'id': None, 'e': now, 'c': 0},
|
||||||
'b': message['body'] if 'body' in message else {},
|
'b': message['body'] if 'body' in message else {},
|
||||||
'k': next_marker + index,
|
'k': next_marker + index,
|
||||||
'tx': None,
|
'tx': None,
|
||||||
@ -815,7 +815,7 @@ class FIFOMessageController(MessageController):
|
|||||||
't': message['ttl'],
|
't': message['ttl'],
|
||||||
'e': now_dt + datetime.timedelta(seconds=message['ttl']),
|
'e': now_dt + datetime.timedelta(seconds=message['ttl']),
|
||||||
'u': client_uuid,
|
'u': client_uuid,
|
||||||
'c': {'id': None, 'e': now},
|
'c': {'id': None, 'e': now, 'c': 0},
|
||||||
'b': message['body'] if 'body' in message else {},
|
'b': message['body'] if 'body' in message else {},
|
||||||
'k': next_marker + index,
|
'k': next_marker + index,
|
||||||
'tx': transaction,
|
'tx': transaction,
|
||||||
|
@ -1010,6 +1010,47 @@ class ClaimControllerTest(ControllerBaseTest):
|
|||||||
{'ttl': 40},
|
{'ttl': 40},
|
||||||
project=self.project)
|
project=self.project)
|
||||||
|
|
||||||
|
def test_dead_letter_queue(self):
|
||||||
|
DLQ_name = "DLQ"
|
||||||
|
meta = {'ttl': 3, 'grace': 3}
|
||||||
|
self.queue_controller.create("DLQ", project=self.project)
|
||||||
|
# Set dead letter queeu metadata
|
||||||
|
metadata = {"_max_claim_count": 2,
|
||||||
|
"_dead_letter_queue": DLQ_name,
|
||||||
|
"_dead_letter_queue_messages_ttl": 9999}
|
||||||
|
self.queue_controller.set_metadata(self.queue_name,
|
||||||
|
metadata,
|
||||||
|
project=self.project)
|
||||||
|
|
||||||
|
new_messages = [{'ttl': 3600, 'body': {"key": "value"}}]
|
||||||
|
|
||||||
|
self.message_controller.post(self.queue_name, new_messages,
|
||||||
|
client_uuid=str(uuid.uuid1()),
|
||||||
|
project=self.project)
|
||||||
|
|
||||||
|
claim_id, messages = self.controller.create(self.queue_name, meta,
|
||||||
|
project=self.project)
|
||||||
|
self.assertIsNotNone(claim_id)
|
||||||
|
self.assertEqual(1, len(list(messages)))
|
||||||
|
time.sleep(5)
|
||||||
|
claim_id, messages = self.controller.create(self.queue_name, meta,
|
||||||
|
project=self.project)
|
||||||
|
self.assertIsNotNone(claim_id)
|
||||||
|
messages = list(messages)
|
||||||
|
self.assertEqual(1, len(messages))
|
||||||
|
time.sleep(5)
|
||||||
|
claim_id, messages = self.controller.create(self.queue_name, meta,
|
||||||
|
project=self.project)
|
||||||
|
self.assertIsNone(claim_id)
|
||||||
|
self.assertEqual(0, len(list(messages)))
|
||||||
|
|
||||||
|
DLQ_messages = self.message_controller.list(DLQ_name,
|
||||||
|
project=self.project,
|
||||||
|
include_claimed=True)
|
||||||
|
expected_msg = list(next(DLQ_messages))[0]
|
||||||
|
self.assertEqual(9999, expected_msg["ttl"])
|
||||||
|
self.assertEqual({"key": "value"}, expected_msg["body"])
|
||||||
|
|
||||||
|
|
||||||
@ddt.ddt
|
@ddt.ddt
|
||||||
class SubscriptionControllerTest(ControllerBaseTest):
|
class SubscriptionControllerTest(ControllerBaseTest):
|
||||||
|
@ -327,6 +327,21 @@ class Validator(object):
|
|||||||
' and must be at least greater than 0.'),
|
' and must be at least greater than 0.'),
|
||||||
self._limits_conf.max_messages_post_size)
|
self._limits_conf.max_messages_post_size)
|
||||||
|
|
||||||
|
max_claim_count = queue_metadata.get('_max_claim_count', None)
|
||||||
|
if max_claim_count and not isinstance(max_claim_count, int):
|
||||||
|
msg = _(u'_max_claim_count must be integer.')
|
||||||
|
raise ValidationFailed(msg)
|
||||||
|
|
||||||
|
dlq_ttl = queue_metadata.get('_dead_letter_queue_messages_ttl', None)
|
||||||
|
if dlq_ttl and not isinstance(dlq_ttl, int):
|
||||||
|
msg = _(u'_dead_letter_queue_messages_ttl must be integer.')
|
||||||
|
raise ValidationFailed(msg)
|
||||||
|
|
||||||
|
if not (MIN_MESSAGE_TTL <= dlq_ttl <=
|
||||||
|
self._limits_conf.max_message_ttl):
|
||||||
|
msg = _(u'The TTL for a message may not exceed {0} seconds, '
|
||||||
|
'and must be at least {1} seconds long.')
|
||||||
|
|
||||||
def queue_purging(self, document):
|
def queue_purging(self, document):
|
||||||
"""Restrictions the resource types to be purged for a queue.
|
"""Restrictions the resource types to be purged for a queue.
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user