
Change-Id: I6c8a4b2c9b62ca2d1c11aa633ad2389786216162 Signed-off-by: ZijianGuo <guozijn@gmail.com>
344 lines
9.9 KiB
Python
344 lines
9.9 KiB
Python
# Copyright (c) 2014 Prashanth Raghu.
|
|
# Copyright (c) 2015 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
|
|
import functools
|
|
import uuid
|
|
|
|
import msgpack
|
|
from oslo_utils import encodeutils
|
|
from oslo_utils import uuidutils
|
|
|
|
MSGENV_FIELD_KEYS = (b'id', b't', b'cr', b'e', b'u', b'c', b'c.e',
|
|
b'c.c', b'd', b'cs')
|
|
SUBENV_FIELD_KEYS = (b'id', b's', b'u', b't', b'e', b'o', b'p', b'c')
|
|
|
|
|
|
# TODO(kgriffs): Make similar classes for claims and queues
|
|
class MessageEnvelope(object):
|
|
"""Encapsulates the message envelope (metadata only, no body).
|
|
|
|
:param id: Message ID in the form of a hexadecimal UUID. If not
|
|
given, one will be automatically generated.
|
|
:param ttl: Message TTL in seconds
|
|
:param created: Message creation time as a UNIX timestamp
|
|
:param client_uuid: UUID of the client that posted the message
|
|
:param claim_id: If claimed, the UUID of the claim. Set to None
|
|
for messages that have never been claimed.
|
|
:param claim_expires: Claim expiration as a UNIX timestamp
|
|
"""
|
|
|
|
__slots__ = [
|
|
'id',
|
|
'ttl',
|
|
'created',
|
|
'expires',
|
|
'client_uuid',
|
|
'claim_id',
|
|
'claim_expires',
|
|
'claim_count',
|
|
'delay_expires',
|
|
'checksum',
|
|
]
|
|
|
|
def __init__(self, **kwargs):
|
|
self.id = _validate_uuid4(kwargs.get('id', uuidutils.generate_uuid()))
|
|
self.ttl = kwargs['ttl']
|
|
self.created = kwargs['created']
|
|
self.expires = kwargs.get('expires', self.created + self.ttl)
|
|
|
|
self.client_uuid = _validate_uuid4(str(kwargs['client_uuid']))
|
|
|
|
self.claim_id = kwargs.get('claim_id')
|
|
if self.claim_id:
|
|
_validate_uuid4(self.claim_id)
|
|
self.claim_expires = kwargs['claim_expires']
|
|
self.claim_count = kwargs.get('claim_count', 0)
|
|
self.delay_expires = kwargs.get('delay_expires', 0)
|
|
self.checksum = kwargs.get('checksum')
|
|
|
|
@staticmethod
|
|
def from_hmap(hmap):
|
|
kwargs = _hmap_to_msgenv_kwargs(hmap)
|
|
return MessageEnvelope(**kwargs)
|
|
|
|
@staticmethod
|
|
def from_redis(mid, client):
|
|
values = client.hmget(mid, MSGENV_FIELD_KEYS)
|
|
|
|
# NOTE(kgriffs): If the key does not exist, redis-py returns
|
|
# an array of None values.
|
|
if values[0] is None:
|
|
return None
|
|
|
|
return _hmap_kv_to_msgenv(MSGENV_FIELD_KEYS, values)
|
|
|
|
@staticmethod
|
|
def from_redis_bulk(message_ids, client):
|
|
with client.pipeline() as pipe:
|
|
for mid in message_ids:
|
|
pipe.hmget(mid, MSGENV_FIELD_KEYS)
|
|
|
|
results = pipe.execute()
|
|
|
|
message_envs = []
|
|
for value_list in results:
|
|
if value_list is None:
|
|
env = None
|
|
else:
|
|
env = _hmap_kv_to_msgenv(MSGENV_FIELD_KEYS, value_list)
|
|
|
|
message_envs.append(env)
|
|
|
|
return message_envs
|
|
|
|
def to_redis(self, pipe):
|
|
hmap = _msgenv_to_hmap(self)
|
|
|
|
pipe.hmset(self.id, hmap)
|
|
pipe.expire(self.id, self.ttl)
|
|
|
|
|
|
class SubscriptionEnvelope(object):
|
|
"""Encapsulates the subscription envelope."""
|
|
|
|
__slots__ = [
|
|
'id',
|
|
'source',
|
|
'subscriber',
|
|
'ttl',
|
|
'expires',
|
|
'options',
|
|
'project',
|
|
'confirmed',
|
|
]
|
|
|
|
def __init__(self, **kwargs):
|
|
self.id = kwargs.get('id', uuidutils.generate_uuid())
|
|
self.source = kwargs['source']
|
|
self.subscriber = kwargs['subscriber']
|
|
self.ttl = kwargs['ttl']
|
|
self.expires = kwargs.get('expires', float('inf'))
|
|
self.options = kwargs['options']
|
|
self.confirmed = kwargs.get('confirmed', 1)
|
|
|
|
@staticmethod
|
|
def from_redis(sid, client):
|
|
values = client.hmget(sid, SUBENV_FIELD_KEYS)
|
|
|
|
# NOTE(kgriffs): If the key does not exist, redis-py returns
|
|
# an array of None values.
|
|
if values[0] is None:
|
|
return None
|
|
|
|
return _hmap_kv_to_subenv(SUBENV_FIELD_KEYS, values)
|
|
|
|
def to_redis(self, pipe):
|
|
hmap = _subenv_to_hmap(self)
|
|
|
|
pipe.hmset(self.id, hmap)
|
|
pipe.expire(self.id, self.ttl)
|
|
|
|
def to_basic(self, now):
|
|
created = self.expires - self.ttl
|
|
is_confirmed = bool(self.confirmed)
|
|
basic_msg = {
|
|
'id': self.id,
|
|
'source': encodeutils.safe_decode(self.source),
|
|
'subscriber': encodeutils.safe_decode(self.subscriber),
|
|
'ttl': self.ttl,
|
|
'age': now - created,
|
|
'options': self.options,
|
|
'confirmed': is_confirmed,
|
|
}
|
|
|
|
return basic_msg
|
|
|
|
|
|
# NOTE(kgriffs): This could have implemented MessageEnvelope functionality
|
|
# by adding an "include_body" param to all the methods, but then you end
|
|
# up with tons of if statements that make the code rather ugly.
|
|
class Message(MessageEnvelope):
|
|
"""Represents an entire message, including envelope and body.
|
|
|
|
:param id: Message ID in the form of a hexadecimal UUID. If not
|
|
given, one will be automatically generated.
|
|
:param ttl: Message TTL in seconds
|
|
:param created: Message creation time as a UNIX timestamp
|
|
:param client_uuid: UUID of the client that posted the message
|
|
:param claim_id: If claimed, the UUID of the claim. Set to None
|
|
for messages that have never been claimed.
|
|
:param claim_expires: Claim expiration as a UNIX timestamp
|
|
:param body: Message payload. Must be serializable to mspack.
|
|
"""
|
|
|
|
__slots__ = MessageEnvelope.__slots__ + ['body']
|
|
|
|
def __init__(self, **kwargs):
|
|
super(Message, self).__init__(**kwargs)
|
|
self.body = kwargs['body']
|
|
|
|
@staticmethod
|
|
def from_hmap(hmap):
|
|
kwargs = _hmap_to_msgenv_kwargs(hmap)
|
|
kwargs['body'] = _unpack(hmap[b'b'])
|
|
|
|
return Message(**kwargs)
|
|
|
|
@staticmethod
|
|
def from_redis(mid, client):
|
|
hmap = client.hgetall(mid)
|
|
return Message.from_hmap(hmap) if hmap else None
|
|
|
|
@staticmethod
|
|
def from_redis_bulk(message_ids, client):
|
|
with client.pipeline() as pipe:
|
|
for mid in message_ids:
|
|
pipe.hgetall(mid)
|
|
|
|
results = pipe.execute()
|
|
|
|
messages = [Message.from_hmap(hmap) if hmap else None
|
|
for hmap in results]
|
|
|
|
return messages
|
|
|
|
def to_redis(self, pipe, include_body=True):
|
|
if not include_body:
|
|
super(Message, self).to_redis(pipe)
|
|
|
|
hmap = _msgenv_to_hmap(self)
|
|
hmap['b'] = _pack(self.body)
|
|
|
|
pipe.hmset(self.id, hmap)
|
|
pipe.expire(self.id, self.ttl)
|
|
|
|
def to_basic(self, now, include_created=False):
|
|
basic_msg = {
|
|
'id': self.id,
|
|
'age': now - self.created,
|
|
'ttl': self.ttl,
|
|
'body': self.body,
|
|
'claim_id': self.claim_id,
|
|
'claim_count': self.claim_count,
|
|
}
|
|
|
|
if include_created:
|
|
created_iso = datetime.datetime.utcfromtimestamp(
|
|
self.created).strftime('%Y-%m-%dT%H:%M:%SZ')
|
|
basic_msg['created'] = created_iso
|
|
if self.checksum:
|
|
basic_msg['checksum'] = self.checksum
|
|
return basic_msg
|
|
|
|
|
|
# ==========================================================================
|
|
# Helpers
|
|
# ==========================================================================
|
|
|
|
|
|
_pack = msgpack.Packer(use_bin_type=True).pack
|
|
_unpack = functools.partial(msgpack.unpackb)
|
|
|
|
|
|
def _hmap_kv_to_msgenv(keys, values):
|
|
hmap = dict(zip(keys, values))
|
|
kwargs = _hmap_to_msgenv_kwargs(hmap)
|
|
return MessageEnvelope(**kwargs)
|
|
|
|
|
|
def _hmap_to_msgenv_kwargs(hmap):
|
|
claim_id = hmap[b'c']
|
|
if claim_id:
|
|
claim_id = encodeutils.safe_decode(claim_id)
|
|
else:
|
|
claim_id = None
|
|
|
|
# NOTE(kgriffs): Under Py3K, redis-py converts all strings
|
|
# into binary. Woohoo!
|
|
res = {
|
|
'id': encodeutils.safe_decode(hmap[b'id']),
|
|
'ttl': int(hmap[b't']),
|
|
'created': int(hmap[b'cr']),
|
|
'expires': int(hmap[b'e']),
|
|
|
|
'client_uuid': encodeutils.safe_decode(hmap[b'u']),
|
|
|
|
'claim_id': claim_id,
|
|
'claim_expires': int(hmap[b'c.e']),
|
|
'claim_count': int(hmap[b'c.c']),
|
|
'delay_expires': int(hmap.get(b'd', 0))
|
|
}
|
|
|
|
checksum = hmap.get(b'cs')
|
|
if checksum:
|
|
res['checksum'] = encodeutils.safe_decode(hmap[b'cs'])
|
|
|
|
return res
|
|
|
|
|
|
def _msgenv_to_hmap(msg):
|
|
res = {
|
|
'id': msg.id,
|
|
't': msg.ttl,
|
|
'cr': msg.created,
|
|
'e': msg.expires,
|
|
'u': msg.client_uuid,
|
|
'c': msg.claim_id or '',
|
|
'c.e': msg.claim_expires,
|
|
'c.c': msg.claim_count,
|
|
'd': msg.delay_expires
|
|
}
|
|
if msg.checksum:
|
|
res['cs'] = msg.checksum
|
|
return res
|
|
|
|
|
|
def _hmap_kv_to_subenv(keys, values):
|
|
hmap = dict(zip(keys, values))
|
|
kwargs = _hmap_to_subenv_kwargs(hmap)
|
|
return SubscriptionEnvelope(**kwargs)
|
|
|
|
|
|
def _hmap_to_subenv_kwargs(hmap):
|
|
# NOTE(kgriffs): Under Py3K, redis-py converts all strings
|
|
# into binary. Woohoo!
|
|
return {
|
|
'id': encodeutils.safe_decode(hmap[b'id']),
|
|
'source': encodeutils.safe_decode(hmap[b's']),
|
|
'subscriber': encodeutils.safe_decode(hmap[b'u']),
|
|
'ttl': int(hmap[b't']),
|
|
'expires': int(hmap[b'e']),
|
|
'options': _unpack(hmap[b'o']),
|
|
'confirmed': int(hmap[b'c'])
|
|
}
|
|
|
|
|
|
def _subenv_to_hmap(msg):
|
|
return {
|
|
'id': msg.id,
|
|
's': msg.source,
|
|
'u': msg.subscriber,
|
|
't': msg.ttl,
|
|
'e': msg.expires,
|
|
'o': msg.options
|
|
}
|
|
|
|
|
|
def _validate_uuid4(_uuid):
|
|
uuid.UUID(str(_uuid), version=4)
|
|
return _uuid
|