Update to latest Oslo rpc modules
* Oslo rpc code now supports heartbeats which will provide Trove reliable connections to guests * Updated the rpc module only via the oslo update procedures Addresses blueprint trove/update-oslo Change-Id: I2e25afc0a20d7d834aaf477a32b9a69aa27a9eba
This commit is contained in:
parent
c8c788e92d
commit
1b0d423928
@ -18,8 +18,11 @@
|
|||||||
|
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import errno
|
||||||
import gc
|
import gc
|
||||||
|
import os
|
||||||
import pprint
|
import pprint
|
||||||
|
import socket
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
@ -28,14 +31,34 @@ import eventlet.backdoor
|
|||||||
import greenlet
|
import greenlet
|
||||||
from oslo.config import cfg
|
from oslo.config import cfg
|
||||||
|
|
||||||
|
from trove.openstack.common.gettextutils import _
|
||||||
|
from trove.openstack.common import log as logging
|
||||||
|
|
||||||
|
help_for_backdoor_port = 'Acceptable ' + \
|
||||||
|
'values are 0, <port> and <start>:<end>, where 0 results in ' + \
|
||||||
|
'listening on a random tcp port number, <port> results in ' + \
|
||||||
|
'listening on the specified port number and not enabling backdoor' + \
|
||||||
|
'if it is in use and <start>:<end> results in listening on the ' + \
|
||||||
|
'smallest unused port number within the specified range of port ' + \
|
||||||
|
'numbers. The chosen port is displayed in the service\'s log file.'
|
||||||
eventlet_backdoor_opts = [
|
eventlet_backdoor_opts = [
|
||||||
cfg.IntOpt('backdoor_port',
|
cfg.StrOpt('backdoor_port',
|
||||||
default=None,
|
default=None,
|
||||||
help='port for eventlet backdoor to listen')
|
help='Enable eventlet backdoor. %s' % help_for_backdoor_port)
|
||||||
]
|
]
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
CONF.register_opts(eventlet_backdoor_opts)
|
CONF.register_opts(eventlet_backdoor_opts)
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class EventletBackdoorConfigValueError(Exception):
|
||||||
|
def __init__(self, port_range, help_msg, ex):
|
||||||
|
msg = ('Invalid backdoor_port configuration %(range)s: %(ex)s. '
|
||||||
|
'%(help)s' %
|
||||||
|
{'range': port_range, 'ex': ex, 'help': help_msg})
|
||||||
|
super(EventletBackdoorConfigValueError, self).__init__(msg)
|
||||||
|
self.port_range = port_range
|
||||||
|
|
||||||
|
|
||||||
def _dont_use_this():
|
def _dont_use_this():
|
||||||
@ -60,6 +83,33 @@ def _print_nativethreads():
|
|||||||
print()
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_port_range(port_range):
|
||||||
|
if ':' not in port_range:
|
||||||
|
start, end = port_range, port_range
|
||||||
|
else:
|
||||||
|
start, end = port_range.split(':', 1)
|
||||||
|
try:
|
||||||
|
start, end = int(start), int(end)
|
||||||
|
if end < start:
|
||||||
|
raise ValueError
|
||||||
|
return start, end
|
||||||
|
except ValueError as ex:
|
||||||
|
raise EventletBackdoorConfigValueError(port_range, ex,
|
||||||
|
help_for_backdoor_port)
|
||||||
|
|
||||||
|
|
||||||
|
def _listen(host, start_port, end_port, listen_func):
|
||||||
|
try_port = start_port
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
return listen_func((host, try_port))
|
||||||
|
except socket.error as exc:
|
||||||
|
if (exc.errno != errno.EADDRINUSE or
|
||||||
|
try_port >= end_port):
|
||||||
|
raise
|
||||||
|
try_port += 1
|
||||||
|
|
||||||
|
|
||||||
def initialize_if_enabled():
|
def initialize_if_enabled():
|
||||||
backdoor_locals = {
|
backdoor_locals = {
|
||||||
'exit': _dont_use_this, # So we don't exit the entire process
|
'exit': _dont_use_this, # So we don't exit the entire process
|
||||||
@ -72,6 +122,8 @@ def initialize_if_enabled():
|
|||||||
if CONF.backdoor_port is None:
|
if CONF.backdoor_port is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
start_port, end_port = _parse_port_range(str(CONF.backdoor_port))
|
||||||
|
|
||||||
# NOTE(johannes): The standard sys.displayhook will print the value of
|
# NOTE(johannes): The standard sys.displayhook will print the value of
|
||||||
# the last expression and set it to __builtin__._, which overwrites
|
# the last expression and set it to __builtin__._, which overwrites
|
||||||
# the __builtin__._ that gettext sets. Let's switch to using pprint
|
# the __builtin__._ that gettext sets. Let's switch to using pprint
|
||||||
@ -82,8 +134,13 @@ def initialize_if_enabled():
|
|||||||
pprint.pprint(val)
|
pprint.pprint(val)
|
||||||
sys.displayhook = displayhook
|
sys.displayhook = displayhook
|
||||||
|
|
||||||
sock = eventlet.listen(('localhost', CONF.backdoor_port))
|
sock = _listen('localhost', start_port, end_port, eventlet.listen)
|
||||||
|
|
||||||
|
# In the case of backdoor port being zero, a port number is assigned by
|
||||||
|
# listen(). In any case, pull the port number out here.
|
||||||
port = sock.getsockname()[1]
|
port = sock.getsockname()[1]
|
||||||
|
LOG.info(_('Eventlet backdoor listening on %(port)s for process %(pid)d') %
|
||||||
|
{'port': port, 'pid': os.getpid()})
|
||||||
eventlet.spawn_n(eventlet.backdoor.backdoor_server, sock,
|
eventlet.spawn_n(eventlet.backdoor.backdoor_server, sock,
|
||||||
locals=backdoor_locals)
|
locals=backdoor_locals)
|
||||||
return port
|
return port
|
||||||
|
@ -19,16 +19,15 @@
|
|||||||
Exception related utilities.
|
Exception related utilities.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import contextlib
|
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from trove.openstack.common.gettextutils import _
|
from trove.openstack.common.gettextutils import _
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
class save_and_reraise_exception(object):
|
||||||
def save_and_reraise_exception():
|
|
||||||
"""Save current exception, run some code and then re-raise.
|
"""Save current exception, run some code and then re-raise.
|
||||||
|
|
||||||
In some cases the exception context can be cleared, resulting in None
|
In some cases the exception context can be cleared, resulting in None
|
||||||
@ -40,12 +39,60 @@ def save_and_reraise_exception():
|
|||||||
To work around this, we save the exception state, run handler code, and
|
To work around this, we save the exception state, run handler code, and
|
||||||
then re-raise the original exception. If another exception occurs, the
|
then re-raise the original exception. If another exception occurs, the
|
||||||
saved exception is logged and the new exception is re-raised.
|
saved exception is logged and the new exception is re-raised.
|
||||||
"""
|
|
||||||
type_, value, tb = sys.exc_info()
|
In some cases the caller may not want to re-raise the exception, and
|
||||||
try:
|
for those circumstances this context provides a reraise flag that
|
||||||
yield
|
can be used to suppress the exception. For example:
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.error(_('Original exception being dropped: %s'),
|
with save_and_reraise_exception() as ctxt:
|
||||||
traceback.format_exception(type_, value, tb))
|
decide_if_need_reraise()
|
||||||
raise
|
if not should_be_reraised:
|
||||||
raise type_, value, tb
|
ctxt.reraise = False
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
self.reraise = True
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self.type_, self.value, self.tb, = sys.exc_info()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
if exc_type is not None:
|
||||||
|
logging.error(_('Original exception being dropped: %s'),
|
||||||
|
traceback.format_exception(self.type_,
|
||||||
|
self.value,
|
||||||
|
self.tb))
|
||||||
|
return False
|
||||||
|
if self.reraise:
|
||||||
|
raise self.type_, self.value, self.tb
|
||||||
|
|
||||||
|
|
||||||
|
def forever_retry_uncaught_exceptions(infunc):
|
||||||
|
def inner_func(*args, **kwargs):
|
||||||
|
last_log_time = 0
|
||||||
|
last_exc_message = None
|
||||||
|
exc_count = 0
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
return infunc(*args, **kwargs)
|
||||||
|
except Exception as exc:
|
||||||
|
if exc.message == last_exc_message:
|
||||||
|
exc_count += 1
|
||||||
|
else:
|
||||||
|
exc_count = 1
|
||||||
|
# Do not log any more frequently than once a minute unless
|
||||||
|
# the exception message changes
|
||||||
|
cur_time = int(time.time())
|
||||||
|
if (cur_time - last_log_time > 60 or
|
||||||
|
exc.message != last_exc_message):
|
||||||
|
logging.exception(
|
||||||
|
_('Unexpected exception occurred %d time(s)... '
|
||||||
|
'retrying.') % exc_count)
|
||||||
|
last_log_time = cur_time
|
||||||
|
last_exc_message = exc.message
|
||||||
|
exc_count = 0
|
||||||
|
# This should be a very rare event. In case it isn't, do
|
||||||
|
# a sleep.
|
||||||
|
time.sleep(1)
|
||||||
|
return inner_func
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
# Copyright 2012 Red Hat, Inc.
|
# Copyright 2012 Red Hat, Inc.
|
||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
|
# Copyright 2013 IBM Corp.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
# 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
|
# not use this file except in compliance with the License. You may obtain
|
||||||
@ -23,8 +24,11 @@ Usual usage in an openstack.common module:
|
|||||||
from trove.openstack.common.gettextutils import _
|
from trove.openstack.common.gettextutils import _
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import copy
|
||||||
import gettext
|
import gettext
|
||||||
|
import logging.handlers
|
||||||
import os
|
import os
|
||||||
|
import UserString
|
||||||
|
|
||||||
_localedir = os.environ.get('trove'.upper() + '_LOCALEDIR')
|
_localedir = os.environ.get('trove'.upper() + '_LOCALEDIR')
|
||||||
_t = gettext.translation('trove', localedir=_localedir, fallback=True)
|
_t = gettext.translation('trove', localedir=_localedir, fallback=True)
|
||||||
@ -48,3 +52,175 @@ def install(domain):
|
|||||||
gettext.install(domain,
|
gettext.install(domain,
|
||||||
localedir=os.environ.get(domain.upper() + '_LOCALEDIR'),
|
localedir=os.environ.get(domain.upper() + '_LOCALEDIR'),
|
||||||
unicode=True)
|
unicode=True)
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Lazy gettext functionality.
|
||||||
|
|
||||||
|
The following is an attempt to introduce a deferred way
|
||||||
|
to do translations on messages in OpenStack. We attempt to
|
||||||
|
override the standard _() function and % (format string) operation
|
||||||
|
to build Message objects that can later be translated when we have
|
||||||
|
more information. Also included is an example LogHandler that
|
||||||
|
translates Messages to an associated locale, effectively allowing
|
||||||
|
many logs, each with their own locale.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def get_lazy_gettext(domain):
|
||||||
|
"""Assemble and return a lazy gettext function for a given domain.
|
||||||
|
|
||||||
|
Factory method for a project/module to get a lazy gettext function
|
||||||
|
for its own translation domain (i.e. nova, glance, cinder, etc.)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _lazy_gettext(msg):
|
||||||
|
"""Create and return a Message object.
|
||||||
|
|
||||||
|
Message encapsulates a string so that we can translate it later when
|
||||||
|
needed.
|
||||||
|
"""
|
||||||
|
return Message(msg, domain)
|
||||||
|
|
||||||
|
return _lazy_gettext
|
||||||
|
|
||||||
|
|
||||||
|
class Message(UserString.UserString, object):
|
||||||
|
"""Class used to encapsulate translatable messages."""
|
||||||
|
def __init__(self, msg, domain):
|
||||||
|
# _msg is the gettext msgid and should never change
|
||||||
|
self._msg = msg
|
||||||
|
self._left_extra_msg = ''
|
||||||
|
self._right_extra_msg = ''
|
||||||
|
self.params = None
|
||||||
|
self.locale = None
|
||||||
|
self.domain = domain
|
||||||
|
|
||||||
|
@property
|
||||||
|
def data(self):
|
||||||
|
# NOTE(mrodden): this should always resolve to a unicode string
|
||||||
|
# that best represents the state of the message currently
|
||||||
|
|
||||||
|
localedir = os.environ.get(self.domain.upper() + '_LOCALEDIR')
|
||||||
|
if self.locale:
|
||||||
|
lang = gettext.translation(self.domain,
|
||||||
|
localedir=localedir,
|
||||||
|
languages=[self.locale],
|
||||||
|
fallback=True)
|
||||||
|
else:
|
||||||
|
# use system locale for translations
|
||||||
|
lang = gettext.translation(self.domain,
|
||||||
|
localedir=localedir,
|
||||||
|
fallback=True)
|
||||||
|
|
||||||
|
full_msg = (self._left_extra_msg +
|
||||||
|
lang.ugettext(self._msg) +
|
||||||
|
self._right_extra_msg)
|
||||||
|
|
||||||
|
if self.params is not None:
|
||||||
|
full_msg = full_msg % self.params
|
||||||
|
|
||||||
|
return unicode(full_msg)
|
||||||
|
|
||||||
|
def _save_parameters(self, other):
|
||||||
|
# we check for None later to see if
|
||||||
|
# we actually have parameters to inject,
|
||||||
|
# so encapsulate if our parameter is actually None
|
||||||
|
if other is None:
|
||||||
|
self.params = (other, )
|
||||||
|
else:
|
||||||
|
self.params = copy.deepcopy(other)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
# overrides to be more string-like
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.data
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.data.encode('utf-8')
|
||||||
|
|
||||||
|
def __getstate__(self):
|
||||||
|
to_copy = ['_msg', '_right_extra_msg', '_left_extra_msg',
|
||||||
|
'domain', 'params', 'locale']
|
||||||
|
new_dict = self.__dict__.fromkeys(to_copy)
|
||||||
|
for attr in to_copy:
|
||||||
|
new_dict[attr] = copy.deepcopy(self.__dict__[attr])
|
||||||
|
|
||||||
|
return new_dict
|
||||||
|
|
||||||
|
def __setstate__(self, state):
|
||||||
|
for (k, v) in state.items():
|
||||||
|
setattr(self, k, v)
|
||||||
|
|
||||||
|
# operator overloads
|
||||||
|
def __add__(self, other):
|
||||||
|
copied = copy.deepcopy(self)
|
||||||
|
copied._right_extra_msg += other.__str__()
|
||||||
|
return copied
|
||||||
|
|
||||||
|
def __radd__(self, other):
|
||||||
|
copied = copy.deepcopy(self)
|
||||||
|
copied._left_extra_msg += other.__str__()
|
||||||
|
return copied
|
||||||
|
|
||||||
|
def __mod__(self, other):
|
||||||
|
# do a format string to catch and raise
|
||||||
|
# any possible KeyErrors from missing parameters
|
||||||
|
self.data % other
|
||||||
|
copied = copy.deepcopy(self)
|
||||||
|
return copied._save_parameters(other)
|
||||||
|
|
||||||
|
def __mul__(self, other):
|
||||||
|
return self.data * other
|
||||||
|
|
||||||
|
def __rmul__(self, other):
|
||||||
|
return other * self.data
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return self.data[key]
|
||||||
|
|
||||||
|
def __getslice__(self, start, end):
|
||||||
|
return self.data.__getslice__(start, end)
|
||||||
|
|
||||||
|
def __getattribute__(self, name):
|
||||||
|
# NOTE(mrodden): handle lossy operations that we can't deal with yet
|
||||||
|
# These override the UserString implementation, since UserString
|
||||||
|
# uses our __class__ attribute to try and build a new message
|
||||||
|
# after running the inner data string through the operation.
|
||||||
|
# At that point, we have lost the gettext message id and can just
|
||||||
|
# safely resolve to a string instead.
|
||||||
|
ops = ['capitalize', 'center', 'decode', 'encode',
|
||||||
|
'expandtabs', 'ljust', 'lstrip', 'replace', 'rjust', 'rstrip',
|
||||||
|
'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
|
||||||
|
if name in ops:
|
||||||
|
return getattr(self.data, name)
|
||||||
|
else:
|
||||||
|
return UserString.UserString.__getattribute__(self, name)
|
||||||
|
|
||||||
|
|
||||||
|
class LocaleHandler(logging.Handler):
|
||||||
|
"""Handler that can have a locale associated to translate Messages.
|
||||||
|
|
||||||
|
A quick example of how to utilize the Message class above.
|
||||||
|
LocaleHandler takes a locale and a target logging.Handler object
|
||||||
|
to forward LogRecord objects to after translating the internal Message.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, locale, target):
|
||||||
|
"""Initialize a LocaleHandler
|
||||||
|
|
||||||
|
:param locale: locale to use for translating messages
|
||||||
|
:param target: logging.Handler object to forward
|
||||||
|
LogRecord objects to after translation
|
||||||
|
"""
|
||||||
|
logging.Handler.__init__(self)
|
||||||
|
self.locale = locale
|
||||||
|
self.target = target
|
||||||
|
|
||||||
|
def emit(self, record):
|
||||||
|
if isinstance(record.msg, Message):
|
||||||
|
# set the locale and resolve to a string
|
||||||
|
record.msg.locale = self.locale
|
||||||
|
|
||||||
|
self.target.emit(record)
|
||||||
|
@ -24,7 +24,7 @@ import traceback
|
|||||||
|
|
||||||
|
|
||||||
def import_class(import_str):
|
def import_class(import_str):
|
||||||
"""Returns a class from a string including module and class"""
|
"""Returns a class from a string including module and class."""
|
||||||
mod_str, _sep, class_str = import_str.rpartition('.')
|
mod_str, _sep, class_str = import_str.rpartition('.')
|
||||||
try:
|
try:
|
||||||
__import__(mod_str)
|
__import__(mod_str)
|
||||||
@ -41,8 +41,9 @@ def import_object(import_str, *args, **kwargs):
|
|||||||
|
|
||||||
|
|
||||||
def import_object_ns(name_space, import_str, *args, **kwargs):
|
def import_object_ns(name_space, import_str, *args, **kwargs):
|
||||||
"""
|
"""Tries to import object from default namespace.
|
||||||
Import a class and return an instance of it, first by trying
|
|
||||||
|
Imports a class and return an instance of it, first by trying
|
||||||
to find the class in a default namespace, then failing back to
|
to find the class in a default namespace, then failing back to
|
||||||
a full path if not found in the default namespace.
|
a full path if not found in the default namespace.
|
||||||
"""
|
"""
|
||||||
|
@ -41,7 +41,8 @@ import json
|
|||||||
import types
|
import types
|
||||||
import xmlrpclib
|
import xmlrpclib
|
||||||
|
|
||||||
#import six
|
import netaddr
|
||||||
|
import six
|
||||||
|
|
||||||
from trove.openstack.common import timeutils
|
from trove.openstack.common import timeutils
|
||||||
|
|
||||||
@ -95,8 +96,7 @@ def to_primitive(value, convert_instances=False, convert_datetime=True,
|
|||||||
# value of itertools.count doesn't get caught by nasty_type_tests
|
# value of itertools.count doesn't get caught by nasty_type_tests
|
||||||
# and results in infinite loop when list(value) is called.
|
# and results in infinite loop when list(value) is called.
|
||||||
if type(value) == itertools.count:
|
if type(value) == itertools.count:
|
||||||
return unicode(value)
|
return six.text_type(value)
|
||||||
# return six.text_type(value)
|
|
||||||
|
|
||||||
# FIXME(vish): Workaround for LP bug 852095. Without this workaround,
|
# FIXME(vish): Workaround for LP bug 852095. Without this workaround,
|
||||||
# tests that raise an exception in a mocked method that
|
# tests that raise an exception in a mocked method that
|
||||||
@ -138,16 +138,16 @@ def to_primitive(value, convert_instances=False, convert_datetime=True,
|
|||||||
# Likely an instance of something. Watch for cycles.
|
# Likely an instance of something. Watch for cycles.
|
||||||
# Ignore class member vars.
|
# Ignore class member vars.
|
||||||
return recursive(value.__dict__, level=level + 1)
|
return recursive(value.__dict__, level=level + 1)
|
||||||
|
elif isinstance(value, netaddr.IPAddress):
|
||||||
|
return six.text_type(value)
|
||||||
else:
|
else:
|
||||||
if any(test(value) for test in _nasty_type_tests):
|
if any(test(value) for test in _nasty_type_tests):
|
||||||
return unicode(value)
|
return six.text_type(value)
|
||||||
# return six.text_type(value)
|
|
||||||
return value
|
return value
|
||||||
except TypeError:
|
except TypeError:
|
||||||
# Class objects are tricky since they may define something like
|
# Class objects are tricky since they may define something like
|
||||||
# __iter__ defined but it isn't callable as list().
|
# __iter__ defined but it isn't callable as list().
|
||||||
return unicode(value)
|
return six.text_type(value)
|
||||||
# return six.text_type(value)
|
|
||||||
|
|
||||||
|
|
||||||
def dumps(value, default=to_primitive, **kwargs):
|
def dumps(value, default=to_primitive, **kwargs):
|
||||||
|
@ -43,9 +43,9 @@ import traceback
|
|||||||
from oslo.config import cfg
|
from oslo.config import cfg
|
||||||
|
|
||||||
from trove.openstack.common.gettextutils import _
|
from trove.openstack.common.gettextutils import _
|
||||||
|
from trove.openstack.common import importutils
|
||||||
from trove.openstack.common import jsonutils
|
from trove.openstack.common import jsonutils
|
||||||
from trove.openstack.common import local
|
from trove.openstack.common import local
|
||||||
from trove.openstack.common import notifier
|
|
||||||
|
|
||||||
|
|
||||||
_DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
_DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
||||||
@ -74,7 +74,8 @@ logging_cli_opts = [
|
|||||||
cfg.StrOpt('log-format',
|
cfg.StrOpt('log-format',
|
||||||
default=None,
|
default=None,
|
||||||
metavar='FORMAT',
|
metavar='FORMAT',
|
||||||
help='A logging.Formatter log message format string which may '
|
help='DEPRECATED. '
|
||||||
|
'A logging.Formatter log message format string which may '
|
||||||
'use any of the available logging.LogRecord attributes. '
|
'use any of the available logging.LogRecord attributes. '
|
||||||
'This option is deprecated. Please use '
|
'This option is deprecated. Please use '
|
||||||
'logging_context_format_string and '
|
'logging_context_format_string and '
|
||||||
@ -322,17 +323,6 @@ class JSONFormatter(logging.Formatter):
|
|||||||
return jsonutils.dumps(message)
|
return jsonutils.dumps(message)
|
||||||
|
|
||||||
|
|
||||||
class PublishErrorsHandler(logging.Handler):
|
|
||||||
def emit(self, record):
|
|
||||||
if ('trove.openstack.common.notifier.log_notifier' in
|
|
||||||
CONF.notification_driver):
|
|
||||||
return
|
|
||||||
notifier.api.notify(None, 'error.publisher',
|
|
||||||
'error_notification',
|
|
||||||
notifier.api.ERROR,
|
|
||||||
dict(error=record.msg))
|
|
||||||
|
|
||||||
|
|
||||||
def _create_logging_excepthook(product_name):
|
def _create_logging_excepthook(product_name):
|
||||||
def logging_excepthook(type, value, tb):
|
def logging_excepthook(type, value, tb):
|
||||||
extra = {}
|
extra = {}
|
||||||
@ -428,7 +418,10 @@ def _setup_logging_from_conf():
|
|||||||
log_root.addHandler(streamlog)
|
log_root.addHandler(streamlog)
|
||||||
|
|
||||||
if CONF.publish_errors:
|
if CONF.publish_errors:
|
||||||
log_root.addHandler(PublishErrorsHandler(logging.ERROR))
|
handler = importutils.import_object(
|
||||||
|
"trove.openstack.common.log_handler.PublishErrorsHandler",
|
||||||
|
logging.ERROR)
|
||||||
|
log_root.addHandler(handler)
|
||||||
|
|
||||||
datefmt = CONF.log_date_format
|
datefmt = CONF.log_date_format
|
||||||
for handler in log_root.handlers:
|
for handler in log_root.handlers:
|
||||||
@ -467,10 +460,11 @@ def getLogger(name='unknown', version='unknown'):
|
|||||||
|
|
||||||
|
|
||||||
def getLazyLogger(name='unknown', version='unknown'):
|
def getLazyLogger(name='unknown', version='unknown'):
|
||||||
"""
|
"""Returns lazy logger.
|
||||||
create a pass-through logger that does not create the real logger
|
|
||||||
|
Creates a pass-through logger that does not create the real logger
|
||||||
until it is really needed and delegates all calls to the real logger
|
until it is really needed and delegates all calls to the real logger
|
||||||
once it is created
|
once it is created.
|
||||||
"""
|
"""
|
||||||
return LazyAdapter(name, version)
|
return LazyAdapter(name, version)
|
||||||
|
|
||||||
|
@ -19,15 +19,12 @@
|
|||||||
Network-related utilities and helper functions.
|
Network-related utilities and helper functions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from trove.openstack.common import log as logging
|
import urlparse
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_host_port(address, default_port=None):
|
def parse_host_port(address, default_port=None):
|
||||||
"""
|
"""Interpret a string as a host:port pair.
|
||||||
Interpret a string as a host:port pair.
|
|
||||||
An IPv6 address MUST be escaped if accompanied by a port,
|
An IPv6 address MUST be escaped if accompanied by a port,
|
||||||
because otherwise ambiguity ensues: 2001:db8:85a3::8a2e:370:7334
|
because otherwise ambiguity ensues: 2001:db8:85a3::8a2e:370:7334
|
||||||
means both [2001:db8:85a3::8a2e:370:7334] and
|
means both [2001:db8:85a3::8a2e:370:7334] and
|
||||||
@ -67,3 +64,18 @@ def parse_host_port(address, default_port=None):
|
|||||||
port = default_port
|
port = default_port
|
||||||
|
|
||||||
return (host, None if port is None else int(port))
|
return (host, None if port is None else int(port))
|
||||||
|
|
||||||
|
|
||||||
|
def urlsplit(url, scheme='', allow_fragments=True):
|
||||||
|
"""Parse a URL using urlparse.urlsplit(), splitting query and fragments.
|
||||||
|
This function papers over Python issue9374 when needed.
|
||||||
|
|
||||||
|
The parameters are the same as urlparse.urlsplit.
|
||||||
|
"""
|
||||||
|
scheme, netloc, path, query, fragment = urlparse.urlsplit(
|
||||||
|
url, scheme, allow_fragments)
|
||||||
|
if allow_fragments and '#' in path:
|
||||||
|
path, fragment = path.split('#', 1)
|
||||||
|
if '?' in path:
|
||||||
|
path, query = path.split('?', 1)
|
||||||
|
return urlparse.SplitResult(scheme, netloc, path, query, fragment)
|
||||||
|
@ -34,10 +34,6 @@ from eventlet import greenpool
|
|||||||
from eventlet import pools
|
from eventlet import pools
|
||||||
from eventlet import queue
|
from eventlet import queue
|
||||||
from eventlet import semaphore
|
from eventlet import semaphore
|
||||||
# TODO(pekowsk): Remove import cfg and below comment in Havana.
|
|
||||||
# This import should no longer be needed when the amqp_rpc_single_reply_queue
|
|
||||||
# option is removed.
|
|
||||||
from oslo.config import cfg
|
|
||||||
|
|
||||||
from trove.openstack.common import excutils
|
from trove.openstack.common import excutils
|
||||||
from trove.openstack.common.gettextutils import _
|
from trove.openstack.common.gettextutils import _
|
||||||
@ -46,16 +42,6 @@ from trove.openstack.common import log as logging
|
|||||||
from trove.openstack.common.rpc import common as rpc_common
|
from trove.openstack.common.rpc import common as rpc_common
|
||||||
|
|
||||||
|
|
||||||
# TODO(pekowski): Remove this option in Havana.
|
|
||||||
amqp_opts = [
|
|
||||||
cfg.BoolOpt('amqp_rpc_single_reply_queue',
|
|
||||||
default=False,
|
|
||||||
help='Enable a fast single reply queue if using AMQP based '
|
|
||||||
'RPC like RabbitMQ or Qpid.'),
|
|
||||||
]
|
|
||||||
|
|
||||||
cfg.CONF.register_opts(amqp_opts)
|
|
||||||
|
|
||||||
UNIQUE_ID = '_unique_id'
|
UNIQUE_ID = '_unique_id'
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -83,7 +69,7 @@ class Pool(pools.Pool):
|
|||||||
# is the above "while loop" gets all the cached connections from the
|
# is the above "while loop" gets all the cached connections from the
|
||||||
# pool and closes them, but never returns them to the pool, a pool
|
# pool and closes them, but never returns them to the pool, a pool
|
||||||
# leak. The unit tests hang waiting for an item to be returned to the
|
# leak. The unit tests hang waiting for an item to be returned to the
|
||||||
# pool. The unit tests get here via the teatDown() method. In the run
|
# pool. The unit tests get here via the tearDown() method. In the run
|
||||||
# time code, it gets here via cleanup() and only appears in service.py
|
# time code, it gets here via cleanup() and only appears in service.py
|
||||||
# just before doing a sys.exit(), so cleanup() only happens once and
|
# just before doing a sys.exit(), so cleanup() only happens once and
|
||||||
# the leakage is not a problem.
|
# the leakage is not a problem.
|
||||||
@ -102,19 +88,19 @@ def get_connection_pool(conf, connection_cls):
|
|||||||
|
|
||||||
|
|
||||||
class ConnectionContext(rpc_common.Connection):
|
class ConnectionContext(rpc_common.Connection):
|
||||||
"""The class that is actually returned to the caller of
|
"""The class that is actually returned to the create_connection() caller.
|
||||||
create_connection(). This is essentially a wrapper around
|
|
||||||
Connection that supports 'with'. It can also return a new
|
This is essentially a wrapper around Connection that supports 'with'.
|
||||||
Connection, or one from a pool. The function will also catch
|
It can also return a new Connection, or one from a pool.
|
||||||
when an instance of this class is to be deleted. With that
|
|
||||||
we can return Connections to the pool on exceptions and so
|
The function will also catch when an instance of this class is to be
|
||||||
forth without making the caller be responsible for catching
|
deleted. With that we can return Connections to the pool on exceptions
|
||||||
them. If possible the function makes sure to return a
|
and so forth without making the caller be responsible for catching them.
|
||||||
connection to the pool.
|
If possible the function makes sure to return a connection to the pool.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, conf, connection_pool, pooled=True, server_params=None):
|
def __init__(self, conf, connection_pool, pooled=True, server_params=None):
|
||||||
"""Create a new connection, or get one from the pool"""
|
"""Create a new connection, or get one from the pool."""
|
||||||
self.connection = None
|
self.connection = None
|
||||||
self.conf = conf
|
self.conf = conf
|
||||||
self.connection_pool = connection_pool
|
self.connection_pool = connection_pool
|
||||||
@ -127,7 +113,7 @@ class ConnectionContext(rpc_common.Connection):
|
|||||||
self.pooled = pooled
|
self.pooled = pooled
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
"""When with ConnectionContext() is used, return self"""
|
"""When with ConnectionContext() is used, return self."""
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def _done(self):
|
def _done(self):
|
||||||
@ -165,17 +151,19 @@ class ConnectionContext(rpc_common.Connection):
|
|||||||
def create_worker(self, topic, proxy, pool_name):
|
def create_worker(self, topic, proxy, pool_name):
|
||||||
self.connection.create_worker(topic, proxy, pool_name)
|
self.connection.create_worker(topic, proxy, pool_name)
|
||||||
|
|
||||||
def join_consumer_pool(self, callback, pool_name, topic, exchange_name):
|
def join_consumer_pool(self, callback, pool_name, topic, exchange_name,
|
||||||
|
ack_on_error=True):
|
||||||
self.connection.join_consumer_pool(callback,
|
self.connection.join_consumer_pool(callback,
|
||||||
pool_name,
|
pool_name,
|
||||||
topic,
|
topic,
|
||||||
exchange_name)
|
exchange_name,
|
||||||
|
ack_on_error)
|
||||||
|
|
||||||
def consume_in_thread(self):
|
def consume_in_thread(self):
|
||||||
self.connection.consume_in_thread()
|
self.connection.consume_in_thread()
|
||||||
|
|
||||||
def __getattr__(self, key):
|
def __getattr__(self, key):
|
||||||
"""Proxy all other calls to the Connection instance"""
|
"""Proxy all other calls to the Connection instance."""
|
||||||
if self.connection:
|
if self.connection:
|
||||||
return getattr(self.connection, key)
|
return getattr(self.connection, key)
|
||||||
else:
|
else:
|
||||||
@ -183,7 +171,7 @@ class ConnectionContext(rpc_common.Connection):
|
|||||||
|
|
||||||
|
|
||||||
class ReplyProxy(ConnectionContext):
|
class ReplyProxy(ConnectionContext):
|
||||||
""" Connection class for RPC replies / callbacks """
|
"""Connection class for RPC replies / callbacks."""
|
||||||
def __init__(self, conf, connection_pool):
|
def __init__(self, conf, connection_pool):
|
||||||
self._call_waiters = {}
|
self._call_waiters = {}
|
||||||
self._num_call_waiters = 0
|
self._num_call_waiters = 0
|
||||||
@ -197,8 +185,10 @@ class ReplyProxy(ConnectionContext):
|
|||||||
msg_id = message_data.pop('_msg_id', None)
|
msg_id = message_data.pop('_msg_id', None)
|
||||||
waiter = self._call_waiters.get(msg_id)
|
waiter = self._call_waiters.get(msg_id)
|
||||||
if not waiter:
|
if not waiter:
|
||||||
LOG.warn(_('no calling threads waiting for msg_id : %s'
|
LOG.warn(_('No calling threads waiting for msg_id : %(msg_id)s'
|
||||||
', message : %s') % (msg_id, message_data))
|
', message : %(data)s'), {'msg_id': msg_id,
|
||||||
|
'data': message_data})
|
||||||
|
LOG.warn(_('_call_waiters: %s') % str(self._call_waiters))
|
||||||
else:
|
else:
|
||||||
waiter.put(message_data)
|
waiter.put(message_data)
|
||||||
|
|
||||||
@ -231,12 +221,7 @@ def msg_reply(conf, msg_id, reply_q, connection_pool, reply=None,
|
|||||||
failure = rpc_common.serialize_remote_exception(failure,
|
failure = rpc_common.serialize_remote_exception(failure,
|
||||||
log_failure)
|
log_failure)
|
||||||
|
|
||||||
try:
|
msg = {'result': reply, 'failure': failure}
|
||||||
msg = {'result': reply, 'failure': failure}
|
|
||||||
except TypeError:
|
|
||||||
msg = {'result': dict((k, repr(v))
|
|
||||||
for k, v in reply.__dict__.iteritems()),
|
|
||||||
'failure': failure}
|
|
||||||
if ending:
|
if ending:
|
||||||
msg['ending'] = True
|
msg['ending'] = True
|
||||||
_add_unique_id(msg)
|
_add_unique_id(msg)
|
||||||
@ -251,7 +236,7 @@ def msg_reply(conf, msg_id, reply_q, connection_pool, reply=None,
|
|||||||
|
|
||||||
|
|
||||||
class RpcContext(rpc_common.CommonRpcContext):
|
class RpcContext(rpc_common.CommonRpcContext):
|
||||||
"""Context that supports replying to a rpc.call"""
|
"""Context that supports replying to a rpc.call."""
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self.msg_id = kwargs.pop('msg_id', None)
|
self.msg_id = kwargs.pop('msg_id', None)
|
||||||
self.reply_q = kwargs.pop('reply_q', None)
|
self.reply_q = kwargs.pop('reply_q', None)
|
||||||
@ -338,8 +323,9 @@ def _add_unique_id(msg):
|
|||||||
|
|
||||||
|
|
||||||
class _ThreadPoolWithWait(object):
|
class _ThreadPoolWithWait(object):
|
||||||
"""Base class for a delayed invocation manager used by
|
"""Base class for a delayed invocation manager.
|
||||||
the Connection class to start up green threads
|
|
||||||
|
Used by the Connection class to start up green threads
|
||||||
to handle incoming messages.
|
to handle incoming messages.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -354,12 +340,14 @@ class _ThreadPoolWithWait(object):
|
|||||||
|
|
||||||
|
|
||||||
class CallbackWrapper(_ThreadPoolWithWait):
|
class CallbackWrapper(_ThreadPoolWithWait):
|
||||||
"""Wraps a straight callback to allow it to be invoked in a green
|
"""Wraps a straight callback.
|
||||||
thread.
|
|
||||||
|
Allows it to be invoked in a green thread.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, conf, callback, connection_pool):
|
def __init__(self, conf, callback, connection_pool):
|
||||||
"""
|
"""Initiates CallbackWrapper object.
|
||||||
|
|
||||||
:param conf: cfg.CONF instance
|
:param conf: cfg.CONF instance
|
||||||
:param callback: a callable (probably a function)
|
:param callback: a callable (probably a function)
|
||||||
:param connection_pool: connection pool as returned by
|
:param connection_pool: connection pool as returned by
|
||||||
@ -490,7 +478,7 @@ class MulticallProxyWaiter(object):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
"""Return a result until we get a reply with an 'ending" flag"""
|
"""Return a result until we get a reply with an 'ending' flag."""
|
||||||
if self._done:
|
if self._done:
|
||||||
raise StopIteration
|
raise StopIteration
|
||||||
while True:
|
while True:
|
||||||
@ -512,61 +500,8 @@ class MulticallProxyWaiter(object):
|
|||||||
yield result
|
yield result
|
||||||
|
|
||||||
|
|
||||||
#TODO(pekowski): Remove MulticallWaiter() in Havana.
|
|
||||||
class MulticallWaiter(object):
|
|
||||||
def __init__(self, conf, connection, timeout):
|
|
||||||
self._connection = connection
|
|
||||||
self._iterator = connection.iterconsume(timeout=timeout or
|
|
||||||
conf.rpc_response_timeout)
|
|
||||||
self._result = None
|
|
||||||
self._done = False
|
|
||||||
self._got_ending = False
|
|
||||||
self._conf = conf
|
|
||||||
self.msg_id_cache = _MsgIdCache()
|
|
||||||
|
|
||||||
def done(self):
|
|
||||||
if self._done:
|
|
||||||
return
|
|
||||||
self._done = True
|
|
||||||
self._iterator.close()
|
|
||||||
self._iterator = None
|
|
||||||
self._connection.close()
|
|
||||||
|
|
||||||
def __call__(self, data):
|
|
||||||
"""The consume() callback will call this. Store the result."""
|
|
||||||
self.msg_id_cache.check_duplicate_message(data)
|
|
||||||
if data['failure']:
|
|
||||||
failure = data['failure']
|
|
||||||
self._result = rpc_common.deserialize_remote_exception(self._conf,
|
|
||||||
failure)
|
|
||||||
|
|
||||||
elif data.get('ending', False):
|
|
||||||
self._got_ending = True
|
|
||||||
else:
|
|
||||||
self._result = data['result']
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
"""Return a result until we get a 'None' response from consumer"""
|
|
||||||
if self._done:
|
|
||||||
raise StopIteration
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
self._iterator.next()
|
|
||||||
except Exception:
|
|
||||||
with excutils.save_and_reraise_exception():
|
|
||||||
self.done()
|
|
||||||
if self._got_ending:
|
|
||||||
self.done()
|
|
||||||
raise StopIteration
|
|
||||||
result = self._result
|
|
||||||
if isinstance(result, Exception):
|
|
||||||
self.done()
|
|
||||||
raise result
|
|
||||||
yield result
|
|
||||||
|
|
||||||
|
|
||||||
def create_connection(conf, new, connection_pool):
|
def create_connection(conf, new, connection_pool):
|
||||||
"""Create a connection"""
|
"""Create a connection."""
|
||||||
return ConnectionContext(conf, connection_pool, pooled=not new)
|
return ConnectionContext(conf, connection_pool, pooled=not new)
|
||||||
|
|
||||||
|
|
||||||
@ -575,14 +510,6 @@ _reply_proxy_create_sem = semaphore.Semaphore()
|
|||||||
|
|
||||||
def multicall(conf, context, topic, msg, timeout, connection_pool):
|
def multicall(conf, context, topic, msg, timeout, connection_pool):
|
||||||
"""Make a call that returns multiple times."""
|
"""Make a call that returns multiple times."""
|
||||||
# TODO(pekowski): Remove all these comments in Havana.
|
|
||||||
# For amqp_rpc_single_reply_queue = False,
|
|
||||||
# Can't use 'with' for multicall, as it returns an iterator
|
|
||||||
# that will continue to use the connection. When it's done,
|
|
||||||
# connection.close() will get called which will put it back into
|
|
||||||
# the pool
|
|
||||||
# For amqp_rpc_single_reply_queue = True,
|
|
||||||
# The 'with' statement is mandatory for closing the connection
|
|
||||||
LOG.debug(_('Making synchronous call on %s ...'), topic)
|
LOG.debug(_('Making synchronous call on %s ...'), topic)
|
||||||
msg_id = uuid.uuid4().hex
|
msg_id = uuid.uuid4().hex
|
||||||
msg.update({'_msg_id': msg_id})
|
msg.update({'_msg_id': msg_id})
|
||||||
@ -590,21 +517,13 @@ def multicall(conf, context, topic, msg, timeout, connection_pool):
|
|||||||
_add_unique_id(msg)
|
_add_unique_id(msg)
|
||||||
pack_context(msg, context)
|
pack_context(msg, context)
|
||||||
|
|
||||||
# TODO(pekowski): Remove this flag and the code under the if clause
|
with _reply_proxy_create_sem:
|
||||||
# in Havana.
|
if not connection_pool.reply_proxy:
|
||||||
if not conf.amqp_rpc_single_reply_queue:
|
connection_pool.reply_proxy = ReplyProxy(conf, connection_pool)
|
||||||
conn = ConnectionContext(conf, connection_pool)
|
msg.update({'_reply_q': connection_pool.reply_proxy.get_reply_q()})
|
||||||
wait_msg = MulticallWaiter(conf, conn, timeout)
|
wait_msg = MulticallProxyWaiter(conf, msg_id, timeout, connection_pool)
|
||||||
conn.declare_direct_consumer(msg_id, wait_msg)
|
with ConnectionContext(conf, connection_pool) as conn:
|
||||||
conn.topic_send(topic, rpc_common.serialize_msg(msg), timeout)
|
conn.topic_send(topic, rpc_common.serialize_msg(msg), timeout)
|
||||||
else:
|
|
||||||
with _reply_proxy_create_sem:
|
|
||||||
if not connection_pool.reply_proxy:
|
|
||||||
connection_pool.reply_proxy = ReplyProxy(conf, connection_pool)
|
|
||||||
msg.update({'_reply_q': connection_pool.reply_proxy.get_reply_q()})
|
|
||||||
wait_msg = MulticallProxyWaiter(conf, msg_id, timeout, connection_pool)
|
|
||||||
with ConnectionContext(conf, connection_pool) as conn:
|
|
||||||
conn.topic_send(topic, rpc_common.serialize_msg(msg), timeout)
|
|
||||||
return wait_msg
|
return wait_msg
|
||||||
|
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ import sys
|
|||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from oslo.config import cfg
|
from oslo.config import cfg
|
||||||
#import six
|
import six
|
||||||
|
|
||||||
from trove.openstack.common.gettextutils import _
|
from trove.openstack.common.gettextutils import _
|
||||||
from trove.openstack.common import importutils
|
from trove.openstack.common import importutils
|
||||||
@ -70,6 +70,8 @@ _RPC_ENVELOPE_VERSION = '2.0'
|
|||||||
_VERSION_KEY = 'oslo.version'
|
_VERSION_KEY = 'oslo.version'
|
||||||
_MESSAGE_KEY = 'oslo.message'
|
_MESSAGE_KEY = 'oslo.message'
|
||||||
|
|
||||||
|
_REMOTE_POSTFIX = '_Remote'
|
||||||
|
|
||||||
|
|
||||||
class RPCException(Exception):
|
class RPCException(Exception):
|
||||||
message = _("An unknown RPC related exception occurred.")
|
message = _("An unknown RPC related exception occurred.")
|
||||||
@ -124,7 +126,8 @@ class Timeout(RPCException):
|
|||||||
'info: "%(info)s"')
|
'info: "%(info)s"')
|
||||||
|
|
||||||
def __init__(self, info=None, topic=None, method=None):
|
def __init__(self, info=None, topic=None, method=None):
|
||||||
"""
|
"""Initiates Timeout object.
|
||||||
|
|
||||||
:param info: Extra info to convey to the user
|
:param info: Extra info to convey to the user
|
||||||
:param topic: The topic that the rpc call was sent to
|
:param topic: The topic that the rpc call was sent to
|
||||||
:param rpc_method_name: The name of the rpc method being
|
:param rpc_method_name: The name of the rpc method being
|
||||||
@ -158,6 +161,10 @@ class UnsupportedRpcEnvelopeVersion(RPCException):
|
|||||||
"not supported by this endpoint.")
|
"not supported by this endpoint.")
|
||||||
|
|
||||||
|
|
||||||
|
class RpcVersionCapError(RPCException):
|
||||||
|
message = _("Specified RPC version cap, %(version_cap)s, is too low")
|
||||||
|
|
||||||
|
|
||||||
class Connection(object):
|
class Connection(object):
|
||||||
"""A connection, returned by rpc.create_connection().
|
"""A connection, returned by rpc.create_connection().
|
||||||
|
|
||||||
@ -217,9 +224,9 @@ class Connection(object):
|
|||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def join_consumer_pool(self, callback, pool_name, topic, exchange_name):
|
def join_consumer_pool(self, callback, pool_name, topic, exchange_name):
|
||||||
"""Register as a member of a group of consumers for a given topic from
|
"""Register as a member of a group of consumers.
|
||||||
the specified exchange.
|
|
||||||
|
|
||||||
|
Uses given topic from the specified exchange.
|
||||||
Exactly one member of a given pool will receive each message.
|
Exactly one member of a given pool will receive each message.
|
||||||
|
|
||||||
A message will be delivered to multiple pools, if more than
|
A message will be delivered to multiple pools, if more than
|
||||||
@ -254,41 +261,20 @@ class Connection(object):
|
|||||||
|
|
||||||
def _safe_log(log_func, msg, msg_data):
|
def _safe_log(log_func, msg, msg_data):
|
||||||
"""Sanitizes the msg_data field before logging."""
|
"""Sanitizes the msg_data field before logging."""
|
||||||
SANITIZE = {'set_admin_password': [('args', 'new_pass')],
|
SANITIZE = ['_context_auth_token', 'auth_token', 'new_pass']
|
||||||
'run_instance': [('args', 'admin_password')],
|
|
||||||
'route_message': [('args', 'message', 'args', 'method_info',
|
|
||||||
'method_kwargs', 'password'),
|
|
||||||
('args', 'message', 'args', 'method_info',
|
|
||||||
'method_kwargs', 'admin_password')]}
|
|
||||||
|
|
||||||
has_method = 'method' in msg_data and msg_data['method'] in SANITIZE
|
def _fix_passwords(d):
|
||||||
has_context_token = '_context_auth_token' in msg_data
|
"""Sanitizes the password fields in the dictionary."""
|
||||||
has_token = 'auth_token' in msg_data
|
for k in d.iterkeys():
|
||||||
|
if k.lower().find('password') != -1:
|
||||||
|
d[k] = '<SANITIZED>'
|
||||||
|
elif k.lower() in SANITIZE:
|
||||||
|
d[k] = '<SANITIZED>'
|
||||||
|
elif isinstance(d[k], dict):
|
||||||
|
_fix_passwords(d[k])
|
||||||
|
return d
|
||||||
|
|
||||||
if not any([has_method, has_context_token, has_token]):
|
return log_func(msg, _fix_passwords(copy.deepcopy(msg_data)))
|
||||||
return log_func(msg, msg_data)
|
|
||||||
|
|
||||||
msg_data = copy.deepcopy(msg_data)
|
|
||||||
|
|
||||||
if has_method:
|
|
||||||
for arg in SANITIZE.get(msg_data['method'], []):
|
|
||||||
try:
|
|
||||||
d = msg_data
|
|
||||||
for elem in arg[:-1]:
|
|
||||||
d = d[elem]
|
|
||||||
d[arg[-1]] = '<SANITIZED>'
|
|
||||||
except KeyError as e:
|
|
||||||
LOG.info(_('Failed to sanitize %(item)s. Key error %(err)s'),
|
|
||||||
{'item': arg,
|
|
||||||
'err': e})
|
|
||||||
|
|
||||||
if has_context_token:
|
|
||||||
msg_data['_context_auth_token'] = '<SANITIZED>'
|
|
||||||
|
|
||||||
if has_token:
|
|
||||||
msg_data['auth_token'] = '<SANITIZED>'
|
|
||||||
|
|
||||||
return log_func(msg, msg_data)
|
|
||||||
|
|
||||||
|
|
||||||
def serialize_remote_exception(failure_info, log_failure=True):
|
def serialize_remote_exception(failure_info, log_failure=True):
|
||||||
@ -300,20 +286,27 @@ def serialize_remote_exception(failure_info, log_failure=True):
|
|||||||
tb = traceback.format_exception(*failure_info)
|
tb = traceback.format_exception(*failure_info)
|
||||||
failure = failure_info[1]
|
failure = failure_info[1]
|
||||||
if log_failure:
|
if log_failure:
|
||||||
LOG.error(_("Returning exception %s to caller"), unicode(failure))
|
LOG.error(_("Returning exception %s to caller"),
|
||||||
# LOG.error(_("Returning exception %s to caller"),
|
six.text_type(failure))
|
||||||
# six.text_type(failure))
|
|
||||||
LOG.error(tb)
|
LOG.error(tb)
|
||||||
|
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
if hasattr(failure, 'kwargs'):
|
if hasattr(failure, 'kwargs'):
|
||||||
kwargs = failure.kwargs
|
kwargs = failure.kwargs
|
||||||
|
|
||||||
|
# NOTE(matiu): With cells, it's possible to re-raise remote, remote
|
||||||
|
# exceptions. Lets turn it back into the original exception type.
|
||||||
|
cls_name = str(failure.__class__.__name__)
|
||||||
|
mod_name = str(failure.__class__.__module__)
|
||||||
|
if (cls_name.endswith(_REMOTE_POSTFIX) and
|
||||||
|
mod_name.endswith(_REMOTE_POSTFIX)):
|
||||||
|
cls_name = cls_name[:-len(_REMOTE_POSTFIX)]
|
||||||
|
mod_name = mod_name[:-len(_REMOTE_POSTFIX)]
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'class': str(failure.__class__.__name__),
|
'class': cls_name,
|
||||||
'module': str(failure.__class__.__module__),
|
'module': mod_name,
|
||||||
'message': unicode(failure),
|
'message': six.text_type(failure),
|
||||||
# 'message': six.text_type(failure),
|
|
||||||
'tb': tb,
|
'tb': tb,
|
||||||
'args': failure.args,
|
'args': failure.args,
|
||||||
'kwargs': kwargs
|
'kwargs': kwargs
|
||||||
@ -349,8 +342,9 @@ def deserialize_remote_exception(conf, data):
|
|||||||
|
|
||||||
ex_type = type(failure)
|
ex_type = type(failure)
|
||||||
str_override = lambda self: message
|
str_override = lambda self: message
|
||||||
new_ex_type = type(ex_type.__name__ + "_Remote", (ex_type,),
|
new_ex_type = type(ex_type.__name__ + _REMOTE_POSTFIX, (ex_type,),
|
||||||
{'__str__': str_override, '__unicode__': str_override})
|
{'__str__': str_override, '__unicode__': str_override})
|
||||||
|
new_ex_type.__module__ = '%s%s' % (module, _REMOTE_POSTFIX)
|
||||||
try:
|
try:
|
||||||
# NOTE(ameade): Dynamically create a new exception type and swap it in
|
# NOTE(ameade): Dynamically create a new exception type and swap it in
|
||||||
# as the new type for the exception. This only works on user defined
|
# as the new type for the exception. This only works on user defined
|
||||||
@ -412,10 +406,11 @@ class CommonRpcContext(object):
|
|||||||
|
|
||||||
|
|
||||||
class ClientException(Exception):
|
class ClientException(Exception):
|
||||||
"""This encapsulates some actual exception that is expected to be
|
"""Encapsulates actual exception expected to be hit by a RPC proxy object.
|
||||||
hit by an RPC proxy object. Merely instantiating it records the
|
|
||||||
current exception information, which will be passed back to the
|
Merely instantiating it records the current exception information, which
|
||||||
RPC client without exceptional logging."""
|
will be passed back to the RPC client without exceptional logging.
|
||||||
|
"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._exc_info = sys.exc_info()
|
self._exc_info = sys.exc_info()
|
||||||
|
|
||||||
@ -432,11 +427,13 @@ def catch_client_exception(exceptions, func, *args, **kwargs):
|
|||||||
|
|
||||||
def client_exceptions(*exceptions):
|
def client_exceptions(*exceptions):
|
||||||
"""Decorator for manager methods that raise expected exceptions.
|
"""Decorator for manager methods that raise expected exceptions.
|
||||||
|
|
||||||
Marking a Manager method with this decorator allows the declaration
|
Marking a Manager method with this decorator allows the declaration
|
||||||
of expected exceptions that the RPC layer should not consider fatal,
|
of expected exceptions that the RPC layer should not consider fatal,
|
||||||
and not log as if they were generated in a real error scenario. Note
|
and not log as if they were generated in a real error scenario. Note
|
||||||
that this will cause listed exceptions to be wrapped in a
|
that this will cause listed exceptions to be wrapped in a
|
||||||
ClientException, which is used internally by the RPC layer."""
|
ClientException, which is used internally by the RPC layer.
|
||||||
|
"""
|
||||||
def outer(func):
|
def outer(func):
|
||||||
def inner(*args, **kwargs):
|
def inner(*args, **kwargs):
|
||||||
return catch_client_exception(exceptions, func, *args, **kwargs)
|
return catch_client_exception(exceptions, func, *args, **kwargs)
|
||||||
|
@ -84,6 +84,7 @@ minimum version that supports the new parameter should be specified.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from trove.openstack.common.rpc import common as rpc_common
|
from trove.openstack.common.rpc import common as rpc_common
|
||||||
|
from trove.openstack.common.rpc import serializer as rpc_serializer
|
||||||
|
|
||||||
|
|
||||||
class RpcDispatcher(object):
|
class RpcDispatcher(object):
|
||||||
@ -93,16 +94,38 @@ class RpcDispatcher(object):
|
|||||||
contains a list of underlying managers that have an API_VERSION attribute.
|
contains a list of underlying managers that have an API_VERSION attribute.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, callbacks):
|
def __init__(self, callbacks, serializer=None):
|
||||||
"""Initialize the rpc dispatcher.
|
"""Initialize the rpc dispatcher.
|
||||||
|
|
||||||
:param callbacks: List of proxy objects that are an instance
|
:param callbacks: List of proxy objects that are an instance
|
||||||
of a class with rpc methods exposed. Each proxy
|
of a class with rpc methods exposed. Each proxy
|
||||||
object should have an RPC_API_VERSION attribute.
|
object should have an RPC_API_VERSION attribute.
|
||||||
|
:param serializer: The Serializer object that will be used to
|
||||||
|
deserialize arguments before the method call and
|
||||||
|
to serialize the result after it returns.
|
||||||
"""
|
"""
|
||||||
self.callbacks = callbacks
|
self.callbacks = callbacks
|
||||||
|
if serializer is None:
|
||||||
|
serializer = rpc_serializer.NoOpSerializer()
|
||||||
|
self.serializer = serializer
|
||||||
super(RpcDispatcher, self).__init__()
|
super(RpcDispatcher, self).__init__()
|
||||||
|
|
||||||
|
def _deserialize_args(self, context, kwargs):
|
||||||
|
"""Helper method called to deserialize args before dispatch.
|
||||||
|
|
||||||
|
This calls our serializer on each argument, returning a new set of
|
||||||
|
args that have been deserialized.
|
||||||
|
|
||||||
|
:param context: The request context
|
||||||
|
:param kwargs: The arguments to be deserialized
|
||||||
|
:returns: A new set of deserialized args
|
||||||
|
"""
|
||||||
|
new_kwargs = dict()
|
||||||
|
for argname, arg in kwargs.iteritems():
|
||||||
|
new_kwargs[argname] = self.serializer.deserialize_entity(context,
|
||||||
|
arg)
|
||||||
|
return new_kwargs
|
||||||
|
|
||||||
def dispatch(self, ctxt, version, method, namespace, **kwargs):
|
def dispatch(self, ctxt, version, method, namespace, **kwargs):
|
||||||
"""Dispatch a message based on a requested version.
|
"""Dispatch a message based on a requested version.
|
||||||
|
|
||||||
@ -145,7 +168,9 @@ class RpcDispatcher(object):
|
|||||||
if not hasattr(proxyobj, method):
|
if not hasattr(proxyobj, method):
|
||||||
continue
|
continue
|
||||||
if is_compatible:
|
if is_compatible:
|
||||||
return getattr(proxyobj, method)(ctxt, **kwargs)
|
kwargs = self._deserialize_args(ctxt, kwargs)
|
||||||
|
result = getattr(proxyobj, method)(ctxt, **kwargs)
|
||||||
|
return self.serializer.serialize_entity(ctxt, result)
|
||||||
|
|
||||||
if had_compatible:
|
if had_compatible:
|
||||||
raise AttributeError("No such RPC function '%s'" % method)
|
raise AttributeError("No such RPC function '%s'" % method)
|
||||||
|
@ -122,7 +122,7 @@ class Connection(object):
|
|||||||
|
|
||||||
|
|
||||||
def create_connection(conf, new=True):
|
def create_connection(conf, new=True):
|
||||||
"""Create a connection"""
|
"""Create a connection."""
|
||||||
return Connection()
|
return Connection()
|
||||||
|
|
||||||
|
|
||||||
@ -179,7 +179,7 @@ def cleanup():
|
|||||||
|
|
||||||
|
|
||||||
def fanout_cast(conf, context, topic, msg):
|
def fanout_cast(conf, context, topic, msg):
|
||||||
"""Cast to all consumers of a topic"""
|
"""Cast to all consumers of a topic."""
|
||||||
check_serialize(msg)
|
check_serialize(msg)
|
||||||
method = msg.get('method')
|
method = msg.get('method')
|
||||||
if not method:
|
if not method:
|
||||||
|
@ -18,7 +18,6 @@ import functools
|
|||||||
import itertools
|
import itertools
|
||||||
import socket
|
import socket
|
||||||
import ssl
|
import ssl
|
||||||
import sys
|
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
@ -30,6 +29,7 @@ import kombu.entity
|
|||||||
import kombu.messaging
|
import kombu.messaging
|
||||||
from oslo.config import cfg
|
from oslo.config import cfg
|
||||||
|
|
||||||
|
from trove.openstack.common import excutils
|
||||||
from trove.openstack.common.gettextutils import _
|
from trove.openstack.common.gettextutils import _
|
||||||
from trove.openstack.common import network_utils
|
from trove.openstack.common import network_utils
|
||||||
from trove.openstack.common.rpc import amqp as rpc_amqp
|
from trove.openstack.common.rpc import amqp as rpc_amqp
|
||||||
@ -82,6 +82,9 @@ kombu_opts = [
|
|||||||
default=0,
|
default=0,
|
||||||
help='maximum retries with trying to connect to RabbitMQ '
|
help='maximum retries with trying to connect to RabbitMQ '
|
||||||
'(the default of 0 implies an infinite retry count)'),
|
'(the default of 0 implies an infinite retry count)'),
|
||||||
|
cfg.IntOpt('rabbit_heartbeat',
|
||||||
|
default=60,
|
||||||
|
help='Seconds between connection keepalive heartbeats'),
|
||||||
cfg.BoolOpt('rabbit_durable_queues',
|
cfg.BoolOpt('rabbit_durable_queues',
|
||||||
default=False,
|
default=False,
|
||||||
help='use durable queues in RabbitMQ'),
|
help='use durable queues in RabbitMQ'),
|
||||||
@ -129,15 +132,46 @@ class ConsumerBase(object):
|
|||||||
self.tag = str(tag)
|
self.tag = str(tag)
|
||||||
self.kwargs = kwargs
|
self.kwargs = kwargs
|
||||||
self.queue = None
|
self.queue = None
|
||||||
|
self.ack_on_error = kwargs.get('ack_on_error', True)
|
||||||
self.reconnect(channel)
|
self.reconnect(channel)
|
||||||
|
|
||||||
def reconnect(self, channel):
|
def reconnect(self, channel):
|
||||||
"""Re-declare the queue after a rabbit reconnect"""
|
"""Re-declare the queue after a rabbit reconnect."""
|
||||||
self.channel = channel
|
self.channel = channel
|
||||||
self.kwargs['channel'] = channel
|
self.kwargs['channel'] = channel
|
||||||
self.queue = kombu.entity.Queue(**self.kwargs)
|
self.queue = kombu.entity.Queue(**self.kwargs)
|
||||||
self.queue.declare()
|
self.queue.declare()
|
||||||
|
|
||||||
|
def _callback_handler(self, message, callback):
|
||||||
|
"""Call callback with deserialized message.
|
||||||
|
|
||||||
|
Messages that are processed without exception are ack'ed.
|
||||||
|
|
||||||
|
If the message processing generates an exception, it will be
|
||||||
|
ack'ed if ack_on_error=True. Otherwise it will be .reject()'ed.
|
||||||
|
Rejection is better than waiting for the message to timeout.
|
||||||
|
Rejected messages are immediately requeued.
|
||||||
|
"""
|
||||||
|
|
||||||
|
ack_msg = False
|
||||||
|
try:
|
||||||
|
msg = rpc_common.deserialize_msg(message.payload)
|
||||||
|
callback(msg)
|
||||||
|
ack_msg = True
|
||||||
|
except Exception:
|
||||||
|
if self.ack_on_error:
|
||||||
|
ack_msg = True
|
||||||
|
LOG.exception(_("Failed to process message"
|
||||||
|
" ... skipping it."))
|
||||||
|
else:
|
||||||
|
LOG.exception(_("Failed to process message"
|
||||||
|
" ... will requeue."))
|
||||||
|
finally:
|
||||||
|
if ack_msg:
|
||||||
|
message.ack()
|
||||||
|
else:
|
||||||
|
message.reject()
|
||||||
|
|
||||||
def consume(self, *args, **kwargs):
|
def consume(self, *args, **kwargs):
|
||||||
"""Actually declare the consumer on the amqp channel. This will
|
"""Actually declare the consumer on the amqp channel. This will
|
||||||
start the flow of messages from the queue. Using the
|
start the flow of messages from the queue. Using the
|
||||||
@ -150,8 +184,6 @@ class ConsumerBase(object):
|
|||||||
If kwargs['nowait'] is True, then this call will block until
|
If kwargs['nowait'] is True, then this call will block until
|
||||||
a message is read.
|
a message is read.
|
||||||
|
|
||||||
Messages will automatically be acked if the callback doesn't
|
|
||||||
raise an exception
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
options = {'consumer_tag': self.tag}
|
options = {'consumer_tag': self.tag}
|
||||||
@ -162,18 +194,12 @@ class ConsumerBase(object):
|
|||||||
|
|
||||||
def _callback(raw_message):
|
def _callback(raw_message):
|
||||||
message = self.channel.message_to_python(raw_message)
|
message = self.channel.message_to_python(raw_message)
|
||||||
try:
|
self._callback_handler(message, callback)
|
||||||
msg = rpc_common.deserialize_msg(message.payload)
|
|
||||||
callback(msg)
|
|
||||||
except Exception:
|
|
||||||
LOG.exception(_("Failed to process message... skipping it."))
|
|
||||||
finally:
|
|
||||||
message.ack()
|
|
||||||
|
|
||||||
self.queue.consume(*args, callback=_callback, **options)
|
self.queue.consume(*args, callback=_callback, **options)
|
||||||
|
|
||||||
def cancel(self):
|
def cancel(self):
|
||||||
"""Cancel the consuming from the queue, if it has started"""
|
"""Cancel the consuming from the queue, if it has started."""
|
||||||
try:
|
try:
|
||||||
self.queue.cancel(self.tag)
|
self.queue.cancel(self.tag)
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
@ -184,7 +210,7 @@ class ConsumerBase(object):
|
|||||||
|
|
||||||
|
|
||||||
class DirectConsumer(ConsumerBase):
|
class DirectConsumer(ConsumerBase):
|
||||||
"""Queue/consumer class for 'direct'"""
|
"""Queue/consumer class for 'direct'."""
|
||||||
|
|
||||||
def __init__(self, conf, channel, msg_id, callback, tag, **kwargs):
|
def __init__(self, conf, channel, msg_id, callback, tag, **kwargs):
|
||||||
"""Init a 'direct' queue.
|
"""Init a 'direct' queue.
|
||||||
@ -216,7 +242,7 @@ class DirectConsumer(ConsumerBase):
|
|||||||
|
|
||||||
|
|
||||||
class TopicConsumer(ConsumerBase):
|
class TopicConsumer(ConsumerBase):
|
||||||
"""Consumer class for 'topic'"""
|
"""Consumer class for 'topic'."""
|
||||||
|
|
||||||
def __init__(self, conf, channel, topic, callback, tag, name=None,
|
def __init__(self, conf, channel, topic, callback, tag, name=None,
|
||||||
exchange_name=None, **kwargs):
|
exchange_name=None, **kwargs):
|
||||||
@ -253,7 +279,7 @@ class TopicConsumer(ConsumerBase):
|
|||||||
|
|
||||||
|
|
||||||
class FanoutConsumer(ConsumerBase):
|
class FanoutConsumer(ConsumerBase):
|
||||||
"""Consumer class for 'fanout'"""
|
"""Consumer class for 'fanout'."""
|
||||||
|
|
||||||
def __init__(self, conf, channel, topic, callback, tag, **kwargs):
|
def __init__(self, conf, channel, topic, callback, tag, **kwargs):
|
||||||
"""Init a 'fanout' queue.
|
"""Init a 'fanout' queue.
|
||||||
@ -286,7 +312,7 @@ class FanoutConsumer(ConsumerBase):
|
|||||||
|
|
||||||
|
|
||||||
class Publisher(object):
|
class Publisher(object):
|
||||||
"""Base Publisher class"""
|
"""Base Publisher class."""
|
||||||
|
|
||||||
def __init__(self, channel, exchange_name, routing_key, **kwargs):
|
def __init__(self, channel, exchange_name, routing_key, **kwargs):
|
||||||
"""Init the Publisher class with the exchange_name, routing_key,
|
"""Init the Publisher class with the exchange_name, routing_key,
|
||||||
@ -298,7 +324,7 @@ class Publisher(object):
|
|||||||
self.reconnect(channel)
|
self.reconnect(channel)
|
||||||
|
|
||||||
def reconnect(self, channel):
|
def reconnect(self, channel):
|
||||||
"""Re-establish the Producer after a rabbit reconnection"""
|
"""Re-establish the Producer after a rabbit reconnection."""
|
||||||
self.exchange = kombu.entity.Exchange(name=self.exchange_name,
|
self.exchange = kombu.entity.Exchange(name=self.exchange_name,
|
||||||
**self.kwargs)
|
**self.kwargs)
|
||||||
self.producer = kombu.messaging.Producer(exchange=self.exchange,
|
self.producer = kombu.messaging.Producer(exchange=self.exchange,
|
||||||
@ -306,7 +332,7 @@ class Publisher(object):
|
|||||||
routing_key=self.routing_key)
|
routing_key=self.routing_key)
|
||||||
|
|
||||||
def send(self, msg, timeout=None):
|
def send(self, msg, timeout=None):
|
||||||
"""Send a message"""
|
"""Send a message."""
|
||||||
if timeout:
|
if timeout:
|
||||||
#
|
#
|
||||||
# AMQP TTL is in milliseconds when set in the header.
|
# AMQP TTL is in milliseconds when set in the header.
|
||||||
@ -317,7 +343,7 @@ class Publisher(object):
|
|||||||
|
|
||||||
|
|
||||||
class DirectPublisher(Publisher):
|
class DirectPublisher(Publisher):
|
||||||
"""Publisher class for 'direct'"""
|
"""Publisher class for 'direct'."""
|
||||||
def __init__(self, conf, channel, msg_id, **kwargs):
|
def __init__(self, conf, channel, msg_id, **kwargs):
|
||||||
"""init a 'direct' publisher.
|
"""init a 'direct' publisher.
|
||||||
|
|
||||||
@ -333,7 +359,7 @@ class DirectPublisher(Publisher):
|
|||||||
|
|
||||||
|
|
||||||
class TopicPublisher(Publisher):
|
class TopicPublisher(Publisher):
|
||||||
"""Publisher class for 'topic'"""
|
"""Publisher class for 'topic'."""
|
||||||
def __init__(self, conf, channel, topic, **kwargs):
|
def __init__(self, conf, channel, topic, **kwargs):
|
||||||
"""init a 'topic' publisher.
|
"""init a 'topic' publisher.
|
||||||
|
|
||||||
@ -352,7 +378,7 @@ class TopicPublisher(Publisher):
|
|||||||
|
|
||||||
|
|
||||||
class FanoutPublisher(Publisher):
|
class FanoutPublisher(Publisher):
|
||||||
"""Publisher class for 'fanout'"""
|
"""Publisher class for 'fanout'."""
|
||||||
def __init__(self, conf, channel, topic, **kwargs):
|
def __init__(self, conf, channel, topic, **kwargs):
|
||||||
"""init a 'fanout' publisher.
|
"""init a 'fanout' publisher.
|
||||||
|
|
||||||
@ -367,7 +393,7 @@ class FanoutPublisher(Publisher):
|
|||||||
|
|
||||||
|
|
||||||
class NotifyPublisher(TopicPublisher):
|
class NotifyPublisher(TopicPublisher):
|
||||||
"""Publisher class for 'notify'"""
|
"""Publisher class for 'notify'."""
|
||||||
|
|
||||||
def __init__(self, conf, channel, topic, **kwargs):
|
def __init__(self, conf, channel, topic, **kwargs):
|
||||||
self.durable = kwargs.pop('durable', conf.rabbit_durable_queues)
|
self.durable = kwargs.pop('durable', conf.rabbit_durable_queues)
|
||||||
@ -426,6 +452,7 @@ class Connection(object):
|
|||||||
'userid': self.conf.rabbit_userid,
|
'userid': self.conf.rabbit_userid,
|
||||||
'password': self.conf.rabbit_password,
|
'password': self.conf.rabbit_password,
|
||||||
'virtual_host': self.conf.rabbit_virtual_host,
|
'virtual_host': self.conf.rabbit_virtual_host,
|
||||||
|
'heartbeat': self.conf.rabbit_heartbeat,
|
||||||
}
|
}
|
||||||
|
|
||||||
for sp_key, value in server_params.iteritems():
|
for sp_key, value in server_params.iteritems():
|
||||||
@ -447,8 +474,9 @@ class Connection(object):
|
|||||||
self.reconnect()
|
self.reconnect()
|
||||||
|
|
||||||
def _fetch_ssl_params(self):
|
def _fetch_ssl_params(self):
|
||||||
"""Handles fetching what ssl params
|
"""Handles fetching what ssl params should be used for the connection
|
||||||
should be used for the connection (if any)"""
|
(if any).
|
||||||
|
"""
|
||||||
ssl_params = dict()
|
ssl_params = dict()
|
||||||
|
|
||||||
# http://docs.python.org/library/ssl.html - ssl.wrap_socket
|
# http://docs.python.org/library/ssl.html - ssl.wrap_socket
|
||||||
@ -536,13 +564,11 @@ class Connection(object):
|
|||||||
log_info.update(params)
|
log_info.update(params)
|
||||||
|
|
||||||
if self.max_retries and attempt == self.max_retries:
|
if self.max_retries and attempt == self.max_retries:
|
||||||
LOG.error(_('Unable to connect to AMQP server on '
|
msg = _('Unable to connect to AMQP server on '
|
||||||
'%(hostname)s:%(port)d after %(max_retries)d '
|
'%(hostname)s:%(port)d after %(max_retries)d '
|
||||||
'tries: %(err_str)s') % log_info)
|
'tries: %(err_str)s') % log_info
|
||||||
# NOTE(comstud): Copied from original code. There's
|
LOG.error(msg)
|
||||||
# really no better recourse because if this was a queue we
|
raise rpc_common.RPCException(msg)
|
||||||
# need to consume on, we have no way to consume anymore.
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if attempt == 1:
|
if attempt == 1:
|
||||||
sleep_time = self.interval_start or 1
|
sleep_time = self.interval_start or 1
|
||||||
@ -578,18 +604,18 @@ class Connection(object):
|
|||||||
self.reconnect()
|
self.reconnect()
|
||||||
|
|
||||||
def get_channel(self):
|
def get_channel(self):
|
||||||
"""Convenience call for bin/clear_rabbit_queues"""
|
"""Convenience call for bin/clear_rabbit_queues."""
|
||||||
return self.channel
|
return self.channel
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""Close/release this connection"""
|
"""Close/release this connection."""
|
||||||
self.cancel_consumer_thread()
|
self.cancel_consumer_thread()
|
||||||
self.wait_on_proxy_callbacks()
|
self.wait_on_proxy_callbacks()
|
||||||
self.connection.release()
|
self.connection.release()
|
||||||
self.connection = None
|
self.connection = None
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
"""Reset a connection so it can be used again"""
|
"""Reset a connection so it can be used again."""
|
||||||
self.cancel_consumer_thread()
|
self.cancel_consumer_thread()
|
||||||
self.wait_on_proxy_callbacks()
|
self.wait_on_proxy_callbacks()
|
||||||
self.channel.close()
|
self.channel.close()
|
||||||
@ -618,7 +644,7 @@ class Connection(object):
|
|||||||
return self.ensure(_connect_error, _declare_consumer)
|
return self.ensure(_connect_error, _declare_consumer)
|
||||||
|
|
||||||
def iterconsume(self, limit=None, timeout=None):
|
def iterconsume(self, limit=None, timeout=None):
|
||||||
"""Return an iterator that will consume from all queues/consumers"""
|
"""Return an iterator that will consume from all queues/consumers."""
|
||||||
|
|
||||||
info = {'do_consume': True}
|
info = {'do_consume': True}
|
||||||
|
|
||||||
@ -634,8 +660,8 @@ class Connection(object):
|
|||||||
|
|
||||||
def _consume():
|
def _consume():
|
||||||
if info['do_consume']:
|
if info['do_consume']:
|
||||||
queues_head = self.consumers[:-1]
|
queues_head = self.consumers[:-1] # not fanout.
|
||||||
queues_tail = self.consumers[-1]
|
queues_tail = self.consumers[-1] # fanout
|
||||||
for queue in queues_head:
|
for queue in queues_head:
|
||||||
queue.consume(nowait=True)
|
queue.consume(nowait=True)
|
||||||
queues_tail.consume(nowait=False)
|
queues_tail.consume(nowait=False)
|
||||||
@ -648,7 +674,7 @@ class Connection(object):
|
|||||||
yield self.ensure(_error_callback, _consume)
|
yield self.ensure(_error_callback, _consume)
|
||||||
|
|
||||||
def cancel_consumer_thread(self):
|
def cancel_consumer_thread(self):
|
||||||
"""Cancel a consumer thread"""
|
"""Cancel a consumer thread."""
|
||||||
if self.consumer_thread is not None:
|
if self.consumer_thread is not None:
|
||||||
self.consumer_thread.kill()
|
self.consumer_thread.kill()
|
||||||
try:
|
try:
|
||||||
@ -663,7 +689,7 @@ class Connection(object):
|
|||||||
proxy_cb.wait()
|
proxy_cb.wait()
|
||||||
|
|
||||||
def publisher_send(self, cls, topic, msg, timeout=None, **kwargs):
|
def publisher_send(self, cls, topic, msg, timeout=None, **kwargs):
|
||||||
"""Send to a publisher based on the publisher class"""
|
"""Send to a publisher based on the publisher class."""
|
||||||
|
|
||||||
def _error_callback(exc):
|
def _error_callback(exc):
|
||||||
log_info = {'topic': topic, 'err_str': str(exc)}
|
log_info = {'topic': topic, 'err_str': str(exc)}
|
||||||
@ -684,36 +710,37 @@ class Connection(object):
|
|||||||
self.declare_consumer(DirectConsumer, topic, callback)
|
self.declare_consumer(DirectConsumer, topic, callback)
|
||||||
|
|
||||||
def declare_topic_consumer(self, topic, callback=None, queue_name=None,
|
def declare_topic_consumer(self, topic, callback=None, queue_name=None,
|
||||||
exchange_name=None):
|
exchange_name=None, ack_on_error=True):
|
||||||
"""Create a 'topic' consumer."""
|
"""Create a 'topic' consumer."""
|
||||||
self.declare_consumer(functools.partial(TopicConsumer,
|
self.declare_consumer(functools.partial(TopicConsumer,
|
||||||
name=queue_name,
|
name=queue_name,
|
||||||
exchange_name=exchange_name,
|
exchange_name=exchange_name,
|
||||||
|
ack_on_error=ack_on_error,
|
||||||
),
|
),
|
||||||
topic, callback)
|
topic, callback)
|
||||||
|
|
||||||
def declare_fanout_consumer(self, topic, callback):
|
def declare_fanout_consumer(self, topic, callback):
|
||||||
"""Create a 'fanout' consumer"""
|
"""Create a 'fanout' consumer."""
|
||||||
self.declare_consumer(FanoutConsumer, topic, callback)
|
self.declare_consumer(FanoutConsumer, topic, callback)
|
||||||
|
|
||||||
def direct_send(self, msg_id, msg):
|
def direct_send(self, msg_id, msg):
|
||||||
"""Send a 'direct' message"""
|
"""Send a 'direct' message."""
|
||||||
self.publisher_send(DirectPublisher, msg_id, msg)
|
self.publisher_send(DirectPublisher, msg_id, msg)
|
||||||
|
|
||||||
def topic_send(self, topic, msg, timeout=None):
|
def topic_send(self, topic, msg, timeout=None):
|
||||||
"""Send a 'topic' message"""
|
"""Send a 'topic' message."""
|
||||||
self.publisher_send(TopicPublisher, topic, msg, timeout)
|
self.publisher_send(TopicPublisher, topic, msg, timeout)
|
||||||
|
|
||||||
def fanout_send(self, topic, msg):
|
def fanout_send(self, topic, msg):
|
||||||
"""Send a 'fanout' message"""
|
"""Send a 'fanout' message."""
|
||||||
self.publisher_send(FanoutPublisher, topic, msg)
|
self.publisher_send(FanoutPublisher, topic, msg)
|
||||||
|
|
||||||
def notify_send(self, topic, msg, **kwargs):
|
def notify_send(self, topic, msg, **kwargs):
|
||||||
"""Send a notify message on a topic"""
|
"""Send a notify message on a topic."""
|
||||||
self.publisher_send(NotifyPublisher, topic, msg, None, **kwargs)
|
self.publisher_send(NotifyPublisher, topic, msg, None, **kwargs)
|
||||||
|
|
||||||
def consume(self, limit=None):
|
def consume(self, limit=None):
|
||||||
"""Consume from all queues/consumers"""
|
"""Consume from all queues/consumers."""
|
||||||
it = self.iterconsume(limit=limit)
|
it = self.iterconsume(limit=limit)
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
@ -722,7 +749,8 @@ class Connection(object):
|
|||||||
return
|
return
|
||||||
|
|
||||||
def consume_in_thread(self):
|
def consume_in_thread(self):
|
||||||
"""Consumer from all queues/consumers in a greenthread"""
|
"""Consumer from all queues/consumers in a greenthread."""
|
||||||
|
@excutils.forever_retry_uncaught_exceptions
|
||||||
def _consumer_thread():
|
def _consumer_thread():
|
||||||
try:
|
try:
|
||||||
self.consume()
|
self.consume()
|
||||||
@ -733,7 +761,7 @@ class Connection(object):
|
|||||||
return self.consumer_thread
|
return self.consumer_thread
|
||||||
|
|
||||||
def create_consumer(self, topic, proxy, fanout=False):
|
def create_consumer(self, topic, proxy, fanout=False):
|
||||||
"""Create a consumer that calls a method in a proxy object"""
|
"""Create a consumer that calls a method in a proxy object."""
|
||||||
proxy_cb = rpc_amqp.ProxyCallback(
|
proxy_cb = rpc_amqp.ProxyCallback(
|
||||||
self.conf, proxy,
|
self.conf, proxy,
|
||||||
rpc_amqp.get_connection_pool(self.conf, Connection))
|
rpc_amqp.get_connection_pool(self.conf, Connection))
|
||||||
@ -745,7 +773,7 @@ class Connection(object):
|
|||||||
self.declare_topic_consumer(topic, proxy_cb)
|
self.declare_topic_consumer(topic, proxy_cb)
|
||||||
|
|
||||||
def create_worker(self, topic, proxy, pool_name):
|
def create_worker(self, topic, proxy, pool_name):
|
||||||
"""Create a worker that calls a method in a proxy object"""
|
"""Create a worker that calls a method in a proxy object."""
|
||||||
proxy_cb = rpc_amqp.ProxyCallback(
|
proxy_cb = rpc_amqp.ProxyCallback(
|
||||||
self.conf, proxy,
|
self.conf, proxy,
|
||||||
rpc_amqp.get_connection_pool(self.conf, Connection))
|
rpc_amqp.get_connection_pool(self.conf, Connection))
|
||||||
@ -753,7 +781,7 @@ class Connection(object):
|
|||||||
self.declare_topic_consumer(topic, proxy_cb, pool_name)
|
self.declare_topic_consumer(topic, proxy_cb, pool_name)
|
||||||
|
|
||||||
def join_consumer_pool(self, callback, pool_name, topic,
|
def join_consumer_pool(self, callback, pool_name, topic,
|
||||||
exchange_name=None):
|
exchange_name=None, ack_on_error=True):
|
||||||
"""Register as a member of a group of consumers for a given topic from
|
"""Register as a member of a group of consumers for a given topic from
|
||||||
the specified exchange.
|
the specified exchange.
|
||||||
|
|
||||||
@ -774,11 +802,12 @@ class Connection(object):
|
|||||||
topic=topic,
|
topic=topic,
|
||||||
exchange_name=exchange_name,
|
exchange_name=exchange_name,
|
||||||
callback=callback_wrapper,
|
callback=callback_wrapper,
|
||||||
|
ack_on_error=ack_on_error,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_connection(conf, new=True):
|
def create_connection(conf, new=True):
|
||||||
"""Create a connection"""
|
"""Create a connection."""
|
||||||
return rpc_amqp.create_connection(
|
return rpc_amqp.create_connection(
|
||||||
conf, new,
|
conf, new,
|
||||||
rpc_amqp.get_connection_pool(conf, Connection))
|
rpc_amqp.get_connection_pool(conf, Connection))
|
||||||
|
@ -24,6 +24,7 @@ import eventlet
|
|||||||
import greenlet
|
import greenlet
|
||||||
from oslo.config import cfg
|
from oslo.config import cfg
|
||||||
|
|
||||||
|
from trove.openstack.common import excutils
|
||||||
from trove.openstack.common.gettextutils import _
|
from trove.openstack.common.gettextutils import _
|
||||||
from trove.openstack.common import importutils
|
from trove.openstack.common import importutils
|
||||||
from trove.openstack.common import jsonutils
|
from trove.openstack.common import jsonutils
|
||||||
@ -31,6 +32,7 @@ from trove.openstack.common import log as logging
|
|||||||
from trove.openstack.common.rpc import amqp as rpc_amqp
|
from trove.openstack.common.rpc import amqp as rpc_amqp
|
||||||
from trove.openstack.common.rpc import common as rpc_common
|
from trove.openstack.common.rpc import common as rpc_common
|
||||||
|
|
||||||
|
qpid_codec = importutils.try_import("qpid.codec010")
|
||||||
qpid_messaging = importutils.try_import("qpid.messaging")
|
qpid_messaging = importutils.try_import("qpid.messaging")
|
||||||
qpid_exceptions = importutils.try_import("qpid.messaging.exceptions")
|
qpid_exceptions = importutils.try_import("qpid.messaging.exceptions")
|
||||||
|
|
||||||
@ -69,6 +71,8 @@ qpid_opts = [
|
|||||||
|
|
||||||
cfg.CONF.register_opts(qpid_opts)
|
cfg.CONF.register_opts(qpid_opts)
|
||||||
|
|
||||||
|
JSON_CONTENT_TYPE = 'application/json; charset=utf8'
|
||||||
|
|
||||||
|
|
||||||
class ConsumerBase(object):
|
class ConsumerBase(object):
|
||||||
"""Consumer base class."""
|
"""Consumer base class."""
|
||||||
@ -115,31 +119,59 @@ class ConsumerBase(object):
|
|||||||
|
|
||||||
self.address = "%s ; %s" % (node_name, jsonutils.dumps(addr_opts))
|
self.address = "%s ; %s" % (node_name, jsonutils.dumps(addr_opts))
|
||||||
|
|
||||||
self.reconnect(session)
|
self.connect(session)
|
||||||
|
|
||||||
|
def connect(self, session):
|
||||||
|
"""Declare the reciever on connect."""
|
||||||
|
self._declare_receiver(session)
|
||||||
|
|
||||||
def reconnect(self, session):
|
def reconnect(self, session):
|
||||||
"""Re-declare the receiver after a qpid reconnect"""
|
"""Re-declare the receiver after a qpid reconnect."""
|
||||||
|
self._declare_receiver(session)
|
||||||
|
|
||||||
|
def _declare_receiver(self, session):
|
||||||
self.session = session
|
self.session = session
|
||||||
self.receiver = session.receiver(self.address)
|
self.receiver = session.receiver(self.address)
|
||||||
self.receiver.capacity = 1
|
self.receiver.capacity = 1
|
||||||
|
|
||||||
|
def _unpack_json_msg(self, msg):
|
||||||
|
"""Load the JSON data in msg if msg.content_type indicates that it
|
||||||
|
is necessary. Put the loaded data back into msg.content and
|
||||||
|
update msg.content_type appropriately.
|
||||||
|
|
||||||
|
A Qpid Message containing a dict will have a content_type of
|
||||||
|
'amqp/map', whereas one containing a string that needs to be converted
|
||||||
|
back from JSON will have a content_type of JSON_CONTENT_TYPE.
|
||||||
|
|
||||||
|
:param msg: a Qpid Message object
|
||||||
|
:returns: None
|
||||||
|
"""
|
||||||
|
if msg.content_type == JSON_CONTENT_TYPE:
|
||||||
|
msg.content = jsonutils.loads(msg.content)
|
||||||
|
msg.content_type = 'amqp/map'
|
||||||
|
|
||||||
def consume(self):
|
def consume(self):
|
||||||
"""Fetch the message and pass it to the callback object"""
|
"""Fetch the message and pass it to the callback object."""
|
||||||
message = self.receiver.fetch()
|
message = self.receiver.fetch()
|
||||||
try:
|
try:
|
||||||
|
self._unpack_json_msg(message)
|
||||||
msg = rpc_common.deserialize_msg(message.content)
|
msg = rpc_common.deserialize_msg(message.content)
|
||||||
self.callback(msg)
|
self.callback(msg)
|
||||||
except Exception:
|
except Exception:
|
||||||
LOG.exception(_("Failed to process message... skipping it."))
|
LOG.exception(_("Failed to process message... skipping it."))
|
||||||
finally:
|
finally:
|
||||||
|
# TODO(sandy): Need support for optional ack_on_error.
|
||||||
self.session.acknowledge(message)
|
self.session.acknowledge(message)
|
||||||
|
|
||||||
def get_receiver(self):
|
def get_receiver(self):
|
||||||
return self.receiver
|
return self.receiver
|
||||||
|
|
||||||
|
def get_node_name(self):
|
||||||
|
return self.address.split(';')[0]
|
||||||
|
|
||||||
|
|
||||||
class DirectConsumer(ConsumerBase):
|
class DirectConsumer(ConsumerBase):
|
||||||
"""Queue/consumer class for 'direct'"""
|
"""Queue/consumer class for 'direct'."""
|
||||||
|
|
||||||
def __init__(self, conf, session, msg_id, callback):
|
def __init__(self, conf, session, msg_id, callback):
|
||||||
"""Init a 'direct' queue.
|
"""Init a 'direct' queue.
|
||||||
@ -157,7 +189,7 @@ class DirectConsumer(ConsumerBase):
|
|||||||
|
|
||||||
|
|
||||||
class TopicConsumer(ConsumerBase):
|
class TopicConsumer(ConsumerBase):
|
||||||
"""Consumer class for 'topic'"""
|
"""Consumer class for 'topic'."""
|
||||||
|
|
||||||
def __init__(self, conf, session, topic, callback, name=None,
|
def __init__(self, conf, session, topic, callback, name=None,
|
||||||
exchange_name=None):
|
exchange_name=None):
|
||||||
@ -177,7 +209,7 @@ class TopicConsumer(ConsumerBase):
|
|||||||
|
|
||||||
|
|
||||||
class FanoutConsumer(ConsumerBase):
|
class FanoutConsumer(ConsumerBase):
|
||||||
"""Consumer class for 'fanout'"""
|
"""Consumer class for 'fanout'."""
|
||||||
|
|
||||||
def __init__(self, conf, session, topic, callback):
|
def __init__(self, conf, session, topic, callback):
|
||||||
"""Init a 'fanout' queue.
|
"""Init a 'fanout' queue.
|
||||||
@ -186,6 +218,7 @@ class FanoutConsumer(ConsumerBase):
|
|||||||
'topic' is the topic to listen on
|
'topic' is the topic to listen on
|
||||||
'callback' is the callback to call when messages are received
|
'callback' is the callback to call when messages are received
|
||||||
"""
|
"""
|
||||||
|
self.conf = conf
|
||||||
|
|
||||||
super(FanoutConsumer, self).__init__(
|
super(FanoutConsumer, self).__init__(
|
||||||
session, callback,
|
session, callback,
|
||||||
@ -194,9 +227,21 @@ class FanoutConsumer(ConsumerBase):
|
|||||||
"%s_fanout_%s" % (topic, uuid.uuid4().hex),
|
"%s_fanout_%s" % (topic, uuid.uuid4().hex),
|
||||||
{"exclusive": True})
|
{"exclusive": True})
|
||||||
|
|
||||||
|
def reconnect(self, session):
|
||||||
|
topic = self.get_node_name()
|
||||||
|
params = {
|
||||||
|
'session': session,
|
||||||
|
'topic': topic,
|
||||||
|
'callback': self.callback,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.__init__(conf=self.conf, **params)
|
||||||
|
|
||||||
|
super(FanoutConsumer, self).reconnect(session)
|
||||||
|
|
||||||
|
|
||||||
class Publisher(object):
|
class Publisher(object):
|
||||||
"""Base Publisher class"""
|
"""Base Publisher class."""
|
||||||
|
|
||||||
def __init__(self, session, node_name, node_opts=None):
|
def __init__(self, session, node_name, node_opts=None):
|
||||||
"""Init the Publisher class with the exchange_name, routing_key,
|
"""Init the Publisher class with the exchange_name, routing_key,
|
||||||
@ -225,16 +270,43 @@ class Publisher(object):
|
|||||||
self.reconnect(session)
|
self.reconnect(session)
|
||||||
|
|
||||||
def reconnect(self, session):
|
def reconnect(self, session):
|
||||||
"""Re-establish the Sender after a reconnection"""
|
"""Re-establish the Sender after a reconnection."""
|
||||||
self.sender = session.sender(self.address)
|
self.sender = session.sender(self.address)
|
||||||
|
|
||||||
|
def _pack_json_msg(self, msg):
|
||||||
|
"""Qpid cannot serialize dicts containing strings longer than 65535
|
||||||
|
characters. This function dumps the message content to a JSON
|
||||||
|
string, which Qpid is able to handle.
|
||||||
|
|
||||||
|
:param msg: May be either a Qpid Message object or a bare dict.
|
||||||
|
:returns: A Qpid Message with its content field JSON encoded.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
msg.content = jsonutils.dumps(msg.content)
|
||||||
|
except AttributeError:
|
||||||
|
# Need to have a Qpid message so we can set the content_type.
|
||||||
|
msg = qpid_messaging.Message(jsonutils.dumps(msg))
|
||||||
|
msg.content_type = JSON_CONTENT_TYPE
|
||||||
|
return msg
|
||||||
|
|
||||||
def send(self, msg):
|
def send(self, msg):
|
||||||
"""Send a message"""
|
"""Send a message."""
|
||||||
|
try:
|
||||||
|
# Check if Qpid can encode the message
|
||||||
|
check_msg = msg
|
||||||
|
if not hasattr(check_msg, 'content_type'):
|
||||||
|
check_msg = qpid_messaging.Message(msg)
|
||||||
|
content_type = check_msg.content_type
|
||||||
|
enc, dec = qpid_messaging.message.get_codec(content_type)
|
||||||
|
enc(check_msg.content)
|
||||||
|
except qpid_codec.CodecException:
|
||||||
|
# This means the message couldn't be serialized as a dict.
|
||||||
|
msg = self._pack_json_msg(msg)
|
||||||
self.sender.send(msg)
|
self.sender.send(msg)
|
||||||
|
|
||||||
|
|
||||||
class DirectPublisher(Publisher):
|
class DirectPublisher(Publisher):
|
||||||
"""Publisher class for 'direct'"""
|
"""Publisher class for 'direct'."""
|
||||||
def __init__(self, conf, session, msg_id):
|
def __init__(self, conf, session, msg_id):
|
||||||
"""Init a 'direct' publisher."""
|
"""Init a 'direct' publisher."""
|
||||||
super(DirectPublisher, self).__init__(session, msg_id,
|
super(DirectPublisher, self).__init__(session, msg_id,
|
||||||
@ -242,7 +314,7 @@ class DirectPublisher(Publisher):
|
|||||||
|
|
||||||
|
|
||||||
class TopicPublisher(Publisher):
|
class TopicPublisher(Publisher):
|
||||||
"""Publisher class for 'topic'"""
|
"""Publisher class for 'topic'."""
|
||||||
def __init__(self, conf, session, topic):
|
def __init__(self, conf, session, topic):
|
||||||
"""init a 'topic' publisher.
|
"""init a 'topic' publisher.
|
||||||
"""
|
"""
|
||||||
@ -252,7 +324,7 @@ class TopicPublisher(Publisher):
|
|||||||
|
|
||||||
|
|
||||||
class FanoutPublisher(Publisher):
|
class FanoutPublisher(Publisher):
|
||||||
"""Publisher class for 'fanout'"""
|
"""Publisher class for 'fanout'."""
|
||||||
def __init__(self, conf, session, topic):
|
def __init__(self, conf, session, topic):
|
||||||
"""init a 'fanout' publisher.
|
"""init a 'fanout' publisher.
|
||||||
"""
|
"""
|
||||||
@ -262,7 +334,7 @@ class FanoutPublisher(Publisher):
|
|||||||
|
|
||||||
|
|
||||||
class NotifyPublisher(Publisher):
|
class NotifyPublisher(Publisher):
|
||||||
"""Publisher class for notifications"""
|
"""Publisher class for notifications."""
|
||||||
def __init__(self, conf, session, topic):
|
def __init__(self, conf, session, topic):
|
||||||
"""init a 'topic' publisher.
|
"""init a 'topic' publisher.
|
||||||
"""
|
"""
|
||||||
@ -330,7 +402,7 @@ class Connection(object):
|
|||||||
return self.consumers[str(receiver)]
|
return self.consumers[str(receiver)]
|
||||||
|
|
||||||
def reconnect(self):
|
def reconnect(self):
|
||||||
"""Handles reconnecting and re-establishing sessions and queues"""
|
"""Handles reconnecting and re-establishing sessions and queues."""
|
||||||
attempt = 0
|
attempt = 0
|
||||||
delay = 1
|
delay = 1
|
||||||
while True:
|
while True:
|
||||||
@ -375,20 +447,26 @@ class Connection(object):
|
|||||||
try:
|
try:
|
||||||
return method(*args, **kwargs)
|
return method(*args, **kwargs)
|
||||||
except (qpid_exceptions.Empty,
|
except (qpid_exceptions.Empty,
|
||||||
qpid_exceptions.ConnectionError), e:
|
qpid_exceptions.ConnectionError) as e:
|
||||||
if error_callback:
|
if error_callback:
|
||||||
error_callback(e)
|
error_callback(e)
|
||||||
self.reconnect()
|
self.reconnect()
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""Close/release this connection"""
|
"""Close/release this connection."""
|
||||||
self.cancel_consumer_thread()
|
self.cancel_consumer_thread()
|
||||||
self.wait_on_proxy_callbacks()
|
self.wait_on_proxy_callbacks()
|
||||||
self.connection.close()
|
try:
|
||||||
|
self.connection.close()
|
||||||
|
except Exception:
|
||||||
|
# NOTE(dripton) Logging exceptions that happen during cleanup just
|
||||||
|
# causes confusion; there's really nothing useful we can do with
|
||||||
|
# them.
|
||||||
|
pass
|
||||||
self.connection = None
|
self.connection = None
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
"""Reset a connection so it can be used again"""
|
"""Reset a connection so it can be used again."""
|
||||||
self.cancel_consumer_thread()
|
self.cancel_consumer_thread()
|
||||||
self.wait_on_proxy_callbacks()
|
self.wait_on_proxy_callbacks()
|
||||||
self.session.close()
|
self.session.close()
|
||||||
@ -412,7 +490,7 @@ class Connection(object):
|
|||||||
return self.ensure(_connect_error, _declare_consumer)
|
return self.ensure(_connect_error, _declare_consumer)
|
||||||
|
|
||||||
def iterconsume(self, limit=None, timeout=None):
|
def iterconsume(self, limit=None, timeout=None):
|
||||||
"""Return an iterator that will consume from all queues/consumers"""
|
"""Return an iterator that will consume from all queues/consumers."""
|
||||||
|
|
||||||
def _error_callback(exc):
|
def _error_callback(exc):
|
||||||
if isinstance(exc, qpid_exceptions.Empty):
|
if isinstance(exc, qpid_exceptions.Empty):
|
||||||
@ -436,7 +514,7 @@ class Connection(object):
|
|||||||
yield self.ensure(_error_callback, _consume)
|
yield self.ensure(_error_callback, _consume)
|
||||||
|
|
||||||
def cancel_consumer_thread(self):
|
def cancel_consumer_thread(self):
|
||||||
"""Cancel a consumer thread"""
|
"""Cancel a consumer thread."""
|
||||||
if self.consumer_thread is not None:
|
if self.consumer_thread is not None:
|
||||||
self.consumer_thread.kill()
|
self.consumer_thread.kill()
|
||||||
try:
|
try:
|
||||||
@ -451,7 +529,7 @@ class Connection(object):
|
|||||||
proxy_cb.wait()
|
proxy_cb.wait()
|
||||||
|
|
||||||
def publisher_send(self, cls, topic, msg):
|
def publisher_send(self, cls, topic, msg):
|
||||||
"""Send to a publisher based on the publisher class"""
|
"""Send to a publisher based on the publisher class."""
|
||||||
|
|
||||||
def _connect_error(exc):
|
def _connect_error(exc):
|
||||||
log_info = {'topic': topic, 'err_str': str(exc)}
|
log_info = {'topic': topic, 'err_str': str(exc)}
|
||||||
@ -481,15 +559,15 @@ class Connection(object):
|
|||||||
topic, callback)
|
topic, callback)
|
||||||
|
|
||||||
def declare_fanout_consumer(self, topic, callback):
|
def declare_fanout_consumer(self, topic, callback):
|
||||||
"""Create a 'fanout' consumer"""
|
"""Create a 'fanout' consumer."""
|
||||||
self.declare_consumer(FanoutConsumer, topic, callback)
|
self.declare_consumer(FanoutConsumer, topic, callback)
|
||||||
|
|
||||||
def direct_send(self, msg_id, msg):
|
def direct_send(self, msg_id, msg):
|
||||||
"""Send a 'direct' message"""
|
"""Send a 'direct' message."""
|
||||||
self.publisher_send(DirectPublisher, msg_id, msg)
|
self.publisher_send(DirectPublisher, msg_id, msg)
|
||||||
|
|
||||||
def topic_send(self, topic, msg, timeout=None):
|
def topic_send(self, topic, msg, timeout=None):
|
||||||
"""Send a 'topic' message"""
|
"""Send a 'topic' message."""
|
||||||
#
|
#
|
||||||
# We want to create a message with attributes, e.g. a TTL. We
|
# We want to create a message with attributes, e.g. a TTL. We
|
||||||
# don't really need to keep 'msg' in its JSON format any longer
|
# don't really need to keep 'msg' in its JSON format any longer
|
||||||
@ -504,15 +582,15 @@ class Connection(object):
|
|||||||
self.publisher_send(TopicPublisher, topic, qpid_message)
|
self.publisher_send(TopicPublisher, topic, qpid_message)
|
||||||
|
|
||||||
def fanout_send(self, topic, msg):
|
def fanout_send(self, topic, msg):
|
||||||
"""Send a 'fanout' message"""
|
"""Send a 'fanout' message."""
|
||||||
self.publisher_send(FanoutPublisher, topic, msg)
|
self.publisher_send(FanoutPublisher, topic, msg)
|
||||||
|
|
||||||
def notify_send(self, topic, msg, **kwargs):
|
def notify_send(self, topic, msg, **kwargs):
|
||||||
"""Send a notify message on a topic"""
|
"""Send a notify message on a topic."""
|
||||||
self.publisher_send(NotifyPublisher, topic, msg)
|
self.publisher_send(NotifyPublisher, topic, msg)
|
||||||
|
|
||||||
def consume(self, limit=None):
|
def consume(self, limit=None):
|
||||||
"""Consume from all queues/consumers"""
|
"""Consume from all queues/consumers."""
|
||||||
it = self.iterconsume(limit=limit)
|
it = self.iterconsume(limit=limit)
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
@ -521,7 +599,8 @@ class Connection(object):
|
|||||||
return
|
return
|
||||||
|
|
||||||
def consume_in_thread(self):
|
def consume_in_thread(self):
|
||||||
"""Consumer from all queues/consumers in a greenthread"""
|
"""Consumer from all queues/consumers in a greenthread."""
|
||||||
|
@excutils.forever_retry_uncaught_exceptions
|
||||||
def _consumer_thread():
|
def _consumer_thread():
|
||||||
try:
|
try:
|
||||||
self.consume()
|
self.consume()
|
||||||
@ -532,7 +611,7 @@ class Connection(object):
|
|||||||
return self.consumer_thread
|
return self.consumer_thread
|
||||||
|
|
||||||
def create_consumer(self, topic, proxy, fanout=False):
|
def create_consumer(self, topic, proxy, fanout=False):
|
||||||
"""Create a consumer that calls a method in a proxy object"""
|
"""Create a consumer that calls a method in a proxy object."""
|
||||||
proxy_cb = rpc_amqp.ProxyCallback(
|
proxy_cb = rpc_amqp.ProxyCallback(
|
||||||
self.conf, proxy,
|
self.conf, proxy,
|
||||||
rpc_amqp.get_connection_pool(self.conf, Connection))
|
rpc_amqp.get_connection_pool(self.conf, Connection))
|
||||||
@ -548,7 +627,7 @@ class Connection(object):
|
|||||||
return consumer
|
return consumer
|
||||||
|
|
||||||
def create_worker(self, topic, proxy, pool_name):
|
def create_worker(self, topic, proxy, pool_name):
|
||||||
"""Create a worker that calls a method in a proxy object"""
|
"""Create a worker that calls a method in a proxy object."""
|
||||||
proxy_cb = rpc_amqp.ProxyCallback(
|
proxy_cb = rpc_amqp.ProxyCallback(
|
||||||
self.conf, proxy,
|
self.conf, proxy,
|
||||||
rpc_amqp.get_connection_pool(self.conf, Connection))
|
rpc_amqp.get_connection_pool(self.conf, Connection))
|
||||||
@ -562,7 +641,7 @@ class Connection(object):
|
|||||||
return consumer
|
return consumer
|
||||||
|
|
||||||
def join_consumer_pool(self, callback, pool_name, topic,
|
def join_consumer_pool(self, callback, pool_name, topic,
|
||||||
exchange_name=None):
|
exchange_name=None, ack_on_error=True):
|
||||||
"""Register as a member of a group of consumers for a given topic from
|
"""Register as a member of a group of consumers for a given topic from
|
||||||
the specified exchange.
|
the specified exchange.
|
||||||
|
|
||||||
@ -591,7 +670,7 @@ class Connection(object):
|
|||||||
|
|
||||||
|
|
||||||
def create_connection(conf, new=True):
|
def create_connection(conf, new=True):
|
||||||
"""Create a connection"""
|
"""Create a connection."""
|
||||||
return rpc_amqp.create_connection(
|
return rpc_amqp.create_connection(
|
||||||
conf, new,
|
conf, new,
|
||||||
rpc_amqp.get_connection_pool(conf, Connection))
|
rpc_amqp.get_connection_pool(conf, Connection))
|
||||||
|
@ -30,7 +30,6 @@ from trove.openstack.common import excutils
|
|||||||
from trove.openstack.common.gettextutils import _
|
from trove.openstack.common.gettextutils import _
|
||||||
from trove.openstack.common import importutils
|
from trove.openstack.common import importutils
|
||||||
from trove.openstack.common import jsonutils
|
from trove.openstack.common import jsonutils
|
||||||
from trove.openstack.common import processutils as utils
|
|
||||||
from trove.openstack.common.rpc import common as rpc_common
|
from trove.openstack.common.rpc import common as rpc_common
|
||||||
|
|
||||||
zmq = importutils.try_import('eventlet.green.zmq')
|
zmq = importutils.try_import('eventlet.green.zmq')
|
||||||
@ -85,8 +84,8 @@ matchmaker = None # memoized matchmaker object
|
|||||||
|
|
||||||
|
|
||||||
def _serialize(data):
|
def _serialize(data):
|
||||||
"""
|
"""Serialization wrapper.
|
||||||
Serialization wrapper
|
|
||||||
We prefer using JSON, but it cannot encode all types.
|
We prefer using JSON, but it cannot encode all types.
|
||||||
Error if a developer passes us bad data.
|
Error if a developer passes us bad data.
|
||||||
"""
|
"""
|
||||||
@ -98,18 +97,15 @@ def _serialize(data):
|
|||||||
|
|
||||||
|
|
||||||
def _deserialize(data):
|
def _deserialize(data):
|
||||||
"""
|
"""Deserialization wrapper."""
|
||||||
Deserialization wrapper
|
|
||||||
"""
|
|
||||||
LOG.debug(_("Deserializing: %s"), data)
|
LOG.debug(_("Deserializing: %s"), data)
|
||||||
return jsonutils.loads(data)
|
return jsonutils.loads(data)
|
||||||
|
|
||||||
|
|
||||||
class ZmqSocket(object):
|
class ZmqSocket(object):
|
||||||
"""
|
"""A tiny wrapper around ZeroMQ.
|
||||||
A tiny wrapper around ZeroMQ to simplify the send/recv protocol
|
|
||||||
and connection management.
|
|
||||||
|
|
||||||
|
Simplifies the send/recv protocol and connection management.
|
||||||
Can be used as a Context (supports the 'with' statement).
|
Can be used as a Context (supports the 'with' statement).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -180,7 +176,7 @@ class ZmqSocket(object):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# We must unsubscribe, or we'll leak descriptors.
|
# We must unsubscribe, or we'll leak descriptors.
|
||||||
if len(self.subscriptions) > 0:
|
if self.subscriptions:
|
||||||
for f in self.subscriptions:
|
for f in self.subscriptions:
|
||||||
try:
|
try:
|
||||||
self.sock.setsockopt(zmq.UNSUBSCRIBE, f)
|
self.sock.setsockopt(zmq.UNSUBSCRIBE, f)
|
||||||
@ -199,26 +195,24 @@ class ZmqSocket(object):
|
|||||||
LOG.error("ZeroMQ socket could not be closed.")
|
LOG.error("ZeroMQ socket could not be closed.")
|
||||||
self.sock = None
|
self.sock = None
|
||||||
|
|
||||||
def recv(self):
|
def recv(self, **kwargs):
|
||||||
if not self.can_recv:
|
if not self.can_recv:
|
||||||
raise RPCException(_("You cannot recv on this socket."))
|
raise RPCException(_("You cannot recv on this socket."))
|
||||||
return self.sock.recv_multipart()
|
return self.sock.recv_multipart(**kwargs)
|
||||||
|
|
||||||
def send(self, data):
|
def send(self, data, **kwargs):
|
||||||
if not self.can_send:
|
if not self.can_send:
|
||||||
raise RPCException(_("You cannot send on this socket."))
|
raise RPCException(_("You cannot send on this socket."))
|
||||||
self.sock.send_multipart(data)
|
self.sock.send_multipart(data, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class ZmqClient(object):
|
class ZmqClient(object):
|
||||||
"""Client for ZMQ sockets."""
|
"""Client for ZMQ sockets."""
|
||||||
|
|
||||||
def __init__(self, addr, socket_type=None, bind=False):
|
def __init__(self, addr):
|
||||||
if socket_type is None:
|
self.outq = ZmqSocket(addr, zmq.PUSH, bind=False)
|
||||||
socket_type = zmq.PUSH
|
|
||||||
self.outq = ZmqSocket(addr, socket_type, bind=bind)
|
|
||||||
|
|
||||||
def cast(self, msg_id, topic, data, envelope=False):
|
def cast(self, msg_id, topic, data, envelope):
|
||||||
msg_id = msg_id or 0
|
msg_id = msg_id or 0
|
||||||
|
|
||||||
if not envelope:
|
if not envelope:
|
||||||
@ -356,16 +350,14 @@ class ConsumerBase(object):
|
|||||||
|
|
||||||
|
|
||||||
class ZmqBaseReactor(ConsumerBase):
|
class ZmqBaseReactor(ConsumerBase):
|
||||||
"""
|
"""A consumer class implementing a centralized casting broker (PULL-PUSH).
|
||||||
A consumer class implementing a
|
|
||||||
centralized casting broker (PULL-PUSH)
|
Used for RoundRobin requests.
|
||||||
for RoundRobin requests.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, conf):
|
def __init__(self, conf):
|
||||||
super(ZmqBaseReactor, self).__init__()
|
super(ZmqBaseReactor, self).__init__()
|
||||||
|
|
||||||
self.mapping = {}
|
|
||||||
self.proxies = {}
|
self.proxies = {}
|
||||||
self.threads = []
|
self.threads = []
|
||||||
self.sockets = []
|
self.sockets = []
|
||||||
@ -373,9 +365,8 @@ class ZmqBaseReactor(ConsumerBase):
|
|||||||
|
|
||||||
self.pool = eventlet.greenpool.GreenPool(conf.rpc_thread_pool_size)
|
self.pool = eventlet.greenpool.GreenPool(conf.rpc_thread_pool_size)
|
||||||
|
|
||||||
def register(self, proxy, in_addr, zmq_type_in, out_addr=None,
|
def register(self, proxy, in_addr, zmq_type_in,
|
||||||
zmq_type_out=None, in_bind=True, out_bind=True,
|
in_bind=True, subscribe=None):
|
||||||
subscribe=None):
|
|
||||||
|
|
||||||
LOG.info(_("Registering reactor"))
|
LOG.info(_("Registering reactor"))
|
||||||
|
|
||||||
@ -391,21 +382,6 @@ class ZmqBaseReactor(ConsumerBase):
|
|||||||
|
|
||||||
LOG.info(_("In reactor registered"))
|
LOG.info(_("In reactor registered"))
|
||||||
|
|
||||||
if not out_addr:
|
|
||||||
return
|
|
||||||
|
|
||||||
if zmq_type_out not in (zmq.PUSH, zmq.PUB):
|
|
||||||
raise RPCException("Bad output socktype")
|
|
||||||
|
|
||||||
# Items push out.
|
|
||||||
outq = ZmqSocket(out_addr, zmq_type_out, bind=out_bind)
|
|
||||||
|
|
||||||
self.mapping[inq] = outq
|
|
||||||
self.mapping[outq] = inq
|
|
||||||
self.sockets.append(outq)
|
|
||||||
|
|
||||||
LOG.info(_("Out reactor registered"))
|
|
||||||
|
|
||||||
def consume_in_thread(self):
|
def consume_in_thread(self):
|
||||||
def _consume(sock):
|
def _consume(sock):
|
||||||
LOG.info(_("Consuming socket"))
|
LOG.info(_("Consuming socket"))
|
||||||
@ -430,10 +406,9 @@ class ZmqBaseReactor(ConsumerBase):
|
|||||||
|
|
||||||
|
|
||||||
class ZmqProxy(ZmqBaseReactor):
|
class ZmqProxy(ZmqBaseReactor):
|
||||||
"""
|
"""A consumer class implementing a topic-based proxy.
|
||||||
A consumer class implementing a
|
|
||||||
topic-based proxy, forwarding to
|
Forwards to IPC sockets.
|
||||||
IPC sockets.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, conf):
|
def __init__(self, conf):
|
||||||
@ -446,11 +421,8 @@ class ZmqProxy(ZmqBaseReactor):
|
|||||||
def consume(self, sock):
|
def consume(self, sock):
|
||||||
ipc_dir = CONF.rpc_zmq_ipc_dir
|
ipc_dir = CONF.rpc_zmq_ipc_dir
|
||||||
|
|
||||||
#TODO(ewindisch): use zero-copy (i.e. references, not copying)
|
data = sock.recv(copy=False)
|
||||||
data = sock.recv()
|
topic = data[1].bytes
|
||||||
topic = data[1]
|
|
||||||
|
|
||||||
LOG.debug(_("CONSUMER GOT %s"), ' '.join(map(pformat, data)))
|
|
||||||
|
|
||||||
if topic.startswith('fanout~'):
|
if topic.startswith('fanout~'):
|
||||||
sock_type = zmq.PUB
|
sock_type = zmq.PUB
|
||||||
@ -492,9 +464,7 @@ class ZmqProxy(ZmqBaseReactor):
|
|||||||
|
|
||||||
while(True):
|
while(True):
|
||||||
data = self.topic_proxy[topic].get()
|
data = self.topic_proxy[topic].get()
|
||||||
out_sock.send(data)
|
out_sock.send(data, copy=False)
|
||||||
LOG.debug(_("ROUTER RELAY-OUT SUCCEEDED %(data)s") %
|
|
||||||
{'data': data})
|
|
||||||
|
|
||||||
wait_sock_creation = eventlet.event.Event()
|
wait_sock_creation = eventlet.event.Event()
|
||||||
eventlet.spawn(publisher, wait_sock_creation)
|
eventlet.spawn(publisher, wait_sock_creation)
|
||||||
@ -507,37 +477,34 @@ class ZmqProxy(ZmqBaseReactor):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
self.topic_proxy[topic].put_nowait(data)
|
self.topic_proxy[topic].put_nowait(data)
|
||||||
LOG.debug(_("ROUTER RELAY-OUT QUEUED %(data)s") %
|
|
||||||
{'data': data})
|
|
||||||
except eventlet.queue.Full:
|
except eventlet.queue.Full:
|
||||||
LOG.error(_("Local per-topic backlog buffer full for topic "
|
LOG.error(_("Local per-topic backlog buffer full for topic "
|
||||||
"%(topic)s. Dropping message.") % {'topic': topic})
|
"%(topic)s. Dropping message.") % {'topic': topic})
|
||||||
|
|
||||||
def consume_in_thread(self):
|
def consume_in_thread(self):
|
||||||
"""Runs the ZmqProxy service"""
|
"""Runs the ZmqProxy service."""
|
||||||
ipc_dir = CONF.rpc_zmq_ipc_dir
|
ipc_dir = CONF.rpc_zmq_ipc_dir
|
||||||
consume_in = "tcp://%s:%s" % \
|
consume_in = "tcp://%s:%s" % \
|
||||||
(CONF.rpc_zmq_bind_address,
|
(CONF.rpc_zmq_bind_address,
|
||||||
CONF.rpc_zmq_port)
|
CONF.rpc_zmq_port)
|
||||||
consumption_proxy = InternalContext(None)
|
consumption_proxy = InternalContext(None)
|
||||||
|
|
||||||
if not os.path.isdir(ipc_dir):
|
try:
|
||||||
try:
|
os.makedirs(ipc_dir)
|
||||||
utils.execute('mkdir', '-p', ipc_dir, run_as_root=True)
|
except os.error:
|
||||||
utils.execute('chown', "%s:%s" % (os.getuid(), os.getgid()),
|
if not os.path.isdir(ipc_dir):
|
||||||
ipc_dir, run_as_root=True)
|
|
||||||
utils.execute('chmod', '750', ipc_dir, run_as_root=True)
|
|
||||||
except utils.ProcessExecutionError:
|
|
||||||
with excutils.save_and_reraise_exception():
|
with excutils.save_and_reraise_exception():
|
||||||
LOG.error(_("Could not create IPC directory %s") %
|
LOG.error(_("Required IPC directory does not exist at"
|
||||||
(ipc_dir, ))
|
" %s") % (ipc_dir, ))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.register(consumption_proxy,
|
self.register(consumption_proxy,
|
||||||
consume_in,
|
consume_in,
|
||||||
zmq.PULL,
|
zmq.PULL)
|
||||||
out_bind=True)
|
|
||||||
except zmq.ZMQError:
|
except zmq.ZMQError:
|
||||||
|
if os.access(ipc_dir, os.X_OK):
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.error(_("Permission denied to IPC directory at"
|
||||||
|
" %s") % (ipc_dir, ))
|
||||||
with excutils.save_and_reraise_exception():
|
with excutils.save_and_reraise_exception():
|
||||||
LOG.error(_("Could not create ZeroMQ receiver daemon. "
|
LOG.error(_("Could not create ZeroMQ receiver daemon. "
|
||||||
"Socket may already be in use."))
|
"Socket may already be in use."))
|
||||||
@ -547,8 +514,9 @@ class ZmqProxy(ZmqBaseReactor):
|
|||||||
|
|
||||||
def unflatten_envelope(packenv):
|
def unflatten_envelope(packenv):
|
||||||
"""Unflattens the RPC envelope.
|
"""Unflattens the RPC envelope.
|
||||||
Takes a list and returns a dictionary.
|
|
||||||
i.e. [1,2,3,4] => {1: 2, 3: 4}
|
Takes a list and returns a dictionary.
|
||||||
|
i.e. [1,2,3,4] => {1: 2, 3: 4}
|
||||||
"""
|
"""
|
||||||
i = iter(packenv)
|
i = iter(packenv)
|
||||||
h = {}
|
h = {}
|
||||||
@ -561,10 +529,9 @@ def unflatten_envelope(packenv):
|
|||||||
|
|
||||||
|
|
||||||
class ZmqReactor(ZmqBaseReactor):
|
class ZmqReactor(ZmqBaseReactor):
|
||||||
"""
|
"""A consumer class implementing a consumer for messages.
|
||||||
A consumer class implementing a
|
|
||||||
consumer for messages. Can also be
|
Can also be used as a 1:1 proxy
|
||||||
used as a 1:1 proxy
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, conf):
|
def __init__(self, conf):
|
||||||
@ -574,11 +541,6 @@ class ZmqReactor(ZmqBaseReactor):
|
|||||||
#TODO(ewindisch): use zero-copy (i.e. references, not copying)
|
#TODO(ewindisch): use zero-copy (i.e. references, not copying)
|
||||||
data = sock.recv()
|
data = sock.recv()
|
||||||
LOG.debug(_("CONSUMER RECEIVED DATA: %s"), data)
|
LOG.debug(_("CONSUMER RECEIVED DATA: %s"), data)
|
||||||
if sock in self.mapping:
|
|
||||||
LOG.debug(_("ROUTER RELAY-OUT %(data)s") % {
|
|
||||||
'data': data})
|
|
||||||
self.mapping[sock].send(data)
|
|
||||||
return
|
|
||||||
|
|
||||||
proxy = self.proxies[sock]
|
proxy = self.proxies[sock]
|
||||||
|
|
||||||
@ -751,10 +713,9 @@ def _call(addr, context, topic, msg, timeout=None,
|
|||||||
|
|
||||||
def _multi_send(method, context, topic, msg, timeout=None,
|
def _multi_send(method, context, topic, msg, timeout=None,
|
||||||
envelope=False, _msg_id=None):
|
envelope=False, _msg_id=None):
|
||||||
"""
|
"""Wraps the sending of messages.
|
||||||
Wraps the sending of messages,
|
|
||||||
dispatches to the matchmaker and sends
|
Dispatches to the matchmaker and sends message to all relevant hosts.
|
||||||
message to all relevant hosts.
|
|
||||||
"""
|
"""
|
||||||
conf = CONF
|
conf = CONF
|
||||||
LOG.debug(_("%(msg)s") % {'msg': ' '.join(map(pformat, (topic, msg)))})
|
LOG.debug(_("%(msg)s") % {'msg': ' '.join(map(pformat, (topic, msg)))})
|
||||||
@ -763,7 +724,7 @@ def _multi_send(method, context, topic, msg, timeout=None,
|
|||||||
LOG.debug(_("Sending message(s) to: %s"), queues)
|
LOG.debug(_("Sending message(s) to: %s"), queues)
|
||||||
|
|
||||||
# Don't stack if we have no matchmaker results
|
# Don't stack if we have no matchmaker results
|
||||||
if len(queues) == 0:
|
if not queues:
|
||||||
LOG.warn(_("No matchmaker results. Not casting."))
|
LOG.warn(_("No matchmaker results. Not casting."))
|
||||||
# While not strictly a timeout, callers know how to handle
|
# While not strictly a timeout, callers know how to handle
|
||||||
# this exception and a timeout isn't too big a lie.
|
# this exception and a timeout isn't too big a lie.
|
||||||
@ -811,8 +772,8 @@ def fanout_cast(conf, context, topic, msg, **kwargs):
|
|||||||
|
|
||||||
|
|
||||||
def notify(conf, context, topic, msg, envelope):
|
def notify(conf, context, topic, msg, envelope):
|
||||||
"""
|
"""Send notification event.
|
||||||
Send notification event.
|
|
||||||
Notifications are sent to topic-priority.
|
Notifications are sent to topic-priority.
|
||||||
This differs from the AMQP drivers which send to topic.priority.
|
This differs from the AMQP drivers which send to topic.priority.
|
||||||
"""
|
"""
|
||||||
@ -846,6 +807,11 @@ def _get_ctxt():
|
|||||||
def _get_matchmaker(*args, **kwargs):
|
def _get_matchmaker(*args, **kwargs):
|
||||||
global matchmaker
|
global matchmaker
|
||||||
if not matchmaker:
|
if not matchmaker:
|
||||||
matchmaker = importutils.import_object(
|
mm = CONF.rpc_zmq_matchmaker
|
||||||
CONF.rpc_zmq_matchmaker, *args, **kwargs)
|
if mm.endswith('matchmaker.MatchMakerRing'):
|
||||||
|
mm.replace('matchmaker', 'matchmaker_ring')
|
||||||
|
LOG.warn(_('rpc_zmq_matchmaker = %(orig)s is deprecated; use'
|
||||||
|
' %(new)s instead') % dict(
|
||||||
|
orig=CONF.rpc_zmq_matchmaker, new=mm))
|
||||||
|
matchmaker = importutils.import_object(mm, *args, **kwargs)
|
||||||
return matchmaker
|
return matchmaker
|
||||||
|
@ -19,8 +19,6 @@ return keys for direct exchanges, per (approximate) AMQP parlance.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
import itertools
|
|
||||||
import json
|
|
||||||
|
|
||||||
import eventlet
|
import eventlet
|
||||||
from oslo.config import cfg
|
from oslo.config import cfg
|
||||||
@ -30,10 +28,6 @@ from trove.openstack.common import log as logging
|
|||||||
|
|
||||||
|
|
||||||
matchmaker_opts = [
|
matchmaker_opts = [
|
||||||
# Matchmaker ring file
|
|
||||||
cfg.StrOpt('matchmaker_ringfile',
|
|
||||||
default='/etc/nova/matchmaker_ring.json',
|
|
||||||
help='Matchmaker ring file (JSON)'),
|
|
||||||
cfg.IntOpt('matchmaker_heartbeat_freq',
|
cfg.IntOpt('matchmaker_heartbeat_freq',
|
||||||
default=300,
|
default=300,
|
||||||
help='Heartbeat frequency'),
|
help='Heartbeat frequency'),
|
||||||
@ -54,8 +48,8 @@ class MatchMakerException(Exception):
|
|||||||
|
|
||||||
|
|
||||||
class Exchange(object):
|
class Exchange(object):
|
||||||
"""
|
"""Implements lookups.
|
||||||
Implements lookups.
|
|
||||||
Subclass this to support hashtables, dns, etc.
|
Subclass this to support hashtables, dns, etc.
|
||||||
"""
|
"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@ -66,9 +60,7 @@ class Exchange(object):
|
|||||||
|
|
||||||
|
|
||||||
class Binding(object):
|
class Binding(object):
|
||||||
"""
|
"""A binding on which to perform a lookup."""
|
||||||
A binding on which to perform a lookup.
|
|
||||||
"""
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -77,10 +69,10 @@ class Binding(object):
|
|||||||
|
|
||||||
|
|
||||||
class MatchMakerBase(object):
|
class MatchMakerBase(object):
|
||||||
"""
|
"""Match Maker Base Class.
|
||||||
Match Maker Base Class.
|
|
||||||
Build off HeartbeatMatchMakerBase if building a
|
Build off HeartbeatMatchMakerBase if building a heartbeat-capable
|
||||||
heartbeat-capable MatchMaker.
|
MatchMaker.
|
||||||
"""
|
"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
# Array of tuples. Index [2] toggles negation, [3] is last-if-true
|
# Array of tuples. Index [2] toggles negation, [3] is last-if-true
|
||||||
@ -90,58 +82,47 @@ class MatchMakerBase(object):
|
|||||||
'registration or heartbeat.')
|
'registration or heartbeat.')
|
||||||
|
|
||||||
def register(self, key, host):
|
def register(self, key, host):
|
||||||
"""
|
"""Register a host on a backend.
|
||||||
Register a host on a backend.
|
|
||||||
Heartbeats, if applicable, may keepalive registration.
|
Heartbeats, if applicable, may keepalive registration.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def ack_alive(self, key, host):
|
def ack_alive(self, key, host):
|
||||||
"""
|
"""Acknowledge that a key.host is alive.
|
||||||
Acknowledge that a key.host is alive.
|
|
||||||
Used internally for updating heartbeats,
|
Used internally for updating heartbeats, but may also be used
|
||||||
but may also be used publically to acknowledge
|
publically to acknowledge a system is alive (i.e. rpc message
|
||||||
a system is alive (i.e. rpc message successfully
|
successfully sent to host)
|
||||||
sent to host)
|
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def is_alive(self, topic, host):
|
def is_alive(self, topic, host):
|
||||||
"""
|
"""Checks if a host is alive."""
|
||||||
Checks if a host is alive.
|
|
||||||
"""
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def expire(self, topic, host):
|
def expire(self, topic, host):
|
||||||
"""
|
"""Explicitly expire a host's registration."""
|
||||||
Explicitly expire a host's registration.
|
|
||||||
"""
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def send_heartbeats(self):
|
def send_heartbeats(self):
|
||||||
"""
|
"""Send all heartbeats.
|
||||||
Send all heartbeats.
|
|
||||||
Use start_heartbeat to spawn a heartbeat greenthread,
|
Use start_heartbeat to spawn a heartbeat greenthread,
|
||||||
which loops this method.
|
which loops this method.
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def unregister(self, key, host):
|
def unregister(self, key, host):
|
||||||
"""
|
"""Unregister a topic."""
|
||||||
Unregister a topic.
|
|
||||||
"""
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def start_heartbeat(self):
|
def start_heartbeat(self):
|
||||||
"""
|
"""Spawn heartbeat greenthread."""
|
||||||
Spawn heartbeat greenthread.
|
|
||||||
"""
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def stop_heartbeat(self):
|
def stop_heartbeat(self):
|
||||||
"""
|
"""Destroys the heartbeat greenthread."""
|
||||||
Destroys the heartbeat greenthread.
|
|
||||||
"""
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def add_binding(self, binding, rule, last=True):
|
def add_binding(self, binding, rule, last=True):
|
||||||
@ -168,10 +149,10 @@ class MatchMakerBase(object):
|
|||||||
|
|
||||||
|
|
||||||
class HeartbeatMatchMakerBase(MatchMakerBase):
|
class HeartbeatMatchMakerBase(MatchMakerBase):
|
||||||
"""
|
"""Base for a heart-beat capable MatchMaker.
|
||||||
Base for a heart-beat capable MatchMaker.
|
|
||||||
Provides common methods for registering,
|
Provides common methods for registering, unregistering, and maintaining
|
||||||
unregistering, and maintaining heartbeats.
|
heartbeats.
|
||||||
"""
|
"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.hosts = set()
|
self.hosts = set()
|
||||||
@ -181,8 +162,8 @@ class HeartbeatMatchMakerBase(MatchMakerBase):
|
|||||||
super(HeartbeatMatchMakerBase, self).__init__()
|
super(HeartbeatMatchMakerBase, self).__init__()
|
||||||
|
|
||||||
def send_heartbeats(self):
|
def send_heartbeats(self):
|
||||||
"""
|
"""Send all heartbeats.
|
||||||
Send all heartbeats.
|
|
||||||
Use start_heartbeat to spawn a heartbeat greenthread,
|
Use start_heartbeat to spawn a heartbeat greenthread,
|
||||||
which loops this method.
|
which loops this method.
|
||||||
"""
|
"""
|
||||||
@ -190,32 +171,31 @@ class HeartbeatMatchMakerBase(MatchMakerBase):
|
|||||||
self.ack_alive(key, host)
|
self.ack_alive(key, host)
|
||||||
|
|
||||||
def ack_alive(self, key, host):
|
def ack_alive(self, key, host):
|
||||||
"""
|
"""Acknowledge that a host.topic is alive.
|
||||||
Acknowledge that a host.topic is alive.
|
|
||||||
Used internally for updating heartbeats,
|
Used internally for updating heartbeats, but may also be used
|
||||||
but may also be used publically to acknowledge
|
publically to acknowledge a system is alive (i.e. rpc message
|
||||||
a system is alive (i.e. rpc message successfully
|
successfully sent to host)
|
||||||
sent to host)
|
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError("Must implement ack_alive")
|
raise NotImplementedError("Must implement ack_alive")
|
||||||
|
|
||||||
def backend_register(self, key, host):
|
def backend_register(self, key, host):
|
||||||
"""
|
"""Implements registration logic.
|
||||||
Implements registration logic.
|
|
||||||
Called by register(self,key,host)
|
Called by register(self,key,host)
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError("Must implement backend_register")
|
raise NotImplementedError("Must implement backend_register")
|
||||||
|
|
||||||
def backend_unregister(self, key, key_host):
|
def backend_unregister(self, key, key_host):
|
||||||
"""
|
"""Implements de-registration logic.
|
||||||
Implements de-registration logic.
|
|
||||||
Called by unregister(self,key,host)
|
Called by unregister(self,key,host)
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError("Must implement backend_unregister")
|
raise NotImplementedError("Must implement backend_unregister")
|
||||||
|
|
||||||
def register(self, key, host):
|
def register(self, key, host):
|
||||||
"""
|
"""Register a host on a backend.
|
||||||
Register a host on a backend.
|
|
||||||
Heartbeats, if applicable, may keepalive registration.
|
Heartbeats, if applicable, may keepalive registration.
|
||||||
"""
|
"""
|
||||||
self.hosts.add(host)
|
self.hosts.add(host)
|
||||||
@ -227,25 +207,24 @@ class HeartbeatMatchMakerBase(MatchMakerBase):
|
|||||||
self.ack_alive(key, host)
|
self.ack_alive(key, host)
|
||||||
|
|
||||||
def unregister(self, key, host):
|
def unregister(self, key, host):
|
||||||
"""
|
"""Unregister a topic."""
|
||||||
Unregister a topic.
|
|
||||||
"""
|
|
||||||
if (key, host) in self.host_topic:
|
if (key, host) in self.host_topic:
|
||||||
del self.host_topic[(key, host)]
|
del self.host_topic[(key, host)]
|
||||||
|
|
||||||
self.hosts.discard(host)
|
self.hosts.discard(host)
|
||||||
self.backend_unregister(key, '.'.join((key, host)))
|
self.backend_unregister(key, '.'.join((key, host)))
|
||||||
|
|
||||||
LOG.info(_("Matchmaker unregistered: %s, %s" % (key, host)))
|
LOG.info(_("Matchmaker unregistered: %(key)s, %(host)s"),
|
||||||
|
{'key': key, 'host': host})
|
||||||
|
|
||||||
def start_heartbeat(self):
|
def start_heartbeat(self):
|
||||||
"""
|
"""Implementation of MatchMakerBase.start_heartbeat.
|
||||||
Implementation of MatchMakerBase.start_heartbeat
|
|
||||||
Launches greenthread looping send_heartbeats(),
|
Launches greenthread looping send_heartbeats(),
|
||||||
yielding for CONF.matchmaker_heartbeat_freq seconds
|
yielding for CONF.matchmaker_heartbeat_freq seconds
|
||||||
between iterations.
|
between iterations.
|
||||||
"""
|
"""
|
||||||
if len(self.hosts) == 0:
|
if not self.hosts:
|
||||||
raise MatchMakerException(
|
raise MatchMakerException(
|
||||||
_("Register before starting heartbeat."))
|
_("Register before starting heartbeat."))
|
||||||
|
|
||||||
@ -257,16 +236,14 @@ class HeartbeatMatchMakerBase(MatchMakerBase):
|
|||||||
self._heart = eventlet.spawn(do_heartbeat)
|
self._heart = eventlet.spawn(do_heartbeat)
|
||||||
|
|
||||||
def stop_heartbeat(self):
|
def stop_heartbeat(self):
|
||||||
"""
|
"""Destroys the heartbeat greenthread."""
|
||||||
Destroys the heartbeat greenthread.
|
|
||||||
"""
|
|
||||||
if self._heart:
|
if self._heart:
|
||||||
self._heart.kill()
|
self._heart.kill()
|
||||||
|
|
||||||
|
|
||||||
class DirectBinding(Binding):
|
class DirectBinding(Binding):
|
||||||
"""
|
"""Specifies a host in the key via a '.' character.
|
||||||
Specifies a host in the key via a '.' character
|
|
||||||
Although dots are used in the key, the behavior here is
|
Although dots are used in the key, the behavior here is
|
||||||
that it maps directly to a host, thus direct.
|
that it maps directly to a host, thus direct.
|
||||||
"""
|
"""
|
||||||
@ -277,8 +254,8 @@ class DirectBinding(Binding):
|
|||||||
|
|
||||||
|
|
||||||
class TopicBinding(Binding):
|
class TopicBinding(Binding):
|
||||||
"""
|
"""Where a 'bare' key without dots.
|
||||||
Where a 'bare' key without dots.
|
|
||||||
AMQP generally considers topic exchanges to be those *with* dots,
|
AMQP generally considers topic exchanges to be those *with* dots,
|
||||||
but we deviate here in terminology as the behavior here matches
|
but we deviate here in terminology as the behavior here matches
|
||||||
that of a topic exchange (whereas where there are dots, behavior
|
that of a topic exchange (whereas where there are dots, behavior
|
||||||
@ -304,67 +281,6 @@ class StubExchange(Exchange):
|
|||||||
return [(key, None)]
|
return [(key, None)]
|
||||||
|
|
||||||
|
|
||||||
class RingExchange(Exchange):
|
|
||||||
"""
|
|
||||||
Match Maker where hosts are loaded from a static file containing
|
|
||||||
a hashmap (JSON formatted).
|
|
||||||
|
|
||||||
__init__ takes optional ring dictionary argument, otherwise
|
|
||||||
loads the ringfile from CONF.mathcmaker_ringfile.
|
|
||||||
"""
|
|
||||||
def __init__(self, ring=None):
|
|
||||||
super(RingExchange, self).__init__()
|
|
||||||
|
|
||||||
if ring:
|
|
||||||
self.ring = ring
|
|
||||||
else:
|
|
||||||
fh = open(CONF.matchmaker_ringfile, 'r')
|
|
||||||
self.ring = json.load(fh)
|
|
||||||
fh.close()
|
|
||||||
|
|
||||||
self.ring0 = {}
|
|
||||||
for k in self.ring.keys():
|
|
||||||
self.ring0[k] = itertools.cycle(self.ring[k])
|
|
||||||
|
|
||||||
def _ring_has(self, key):
|
|
||||||
if key in self.ring0:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class RoundRobinRingExchange(RingExchange):
|
|
||||||
"""A Topic Exchange based on a hashmap."""
|
|
||||||
def __init__(self, ring=None):
|
|
||||||
super(RoundRobinRingExchange, self).__init__(ring)
|
|
||||||
|
|
||||||
def run(self, key):
|
|
||||||
if not self._ring_has(key):
|
|
||||||
LOG.warn(
|
|
||||||
_("No key defining hosts for topic '%s', "
|
|
||||||
"see ringfile") % (key, )
|
|
||||||
)
|
|
||||||
return []
|
|
||||||
host = next(self.ring0[key])
|
|
||||||
return [(key + '.' + host, host)]
|
|
||||||
|
|
||||||
|
|
||||||
class FanoutRingExchange(RingExchange):
|
|
||||||
"""Fanout Exchange based on a hashmap."""
|
|
||||||
def __init__(self, ring=None):
|
|
||||||
super(FanoutRingExchange, self).__init__(ring)
|
|
||||||
|
|
||||||
def run(self, key):
|
|
||||||
# Assume starts with "fanout~", strip it for lookup.
|
|
||||||
nkey = key.split('fanout~')[1:][0]
|
|
||||||
if not self._ring_has(nkey):
|
|
||||||
LOG.warn(
|
|
||||||
_("No key defining hosts for topic '%s', "
|
|
||||||
"see ringfile") % (nkey, )
|
|
||||||
)
|
|
||||||
return []
|
|
||||||
return map(lambda x: (key + '.' + x, x), self.ring[nkey])
|
|
||||||
|
|
||||||
|
|
||||||
class LocalhostExchange(Exchange):
|
class LocalhostExchange(Exchange):
|
||||||
"""Exchange where all direct topics are local."""
|
"""Exchange where all direct topics are local."""
|
||||||
def __init__(self, host='localhost'):
|
def __init__(self, host='localhost'):
|
||||||
@ -376,8 +292,8 @@ class LocalhostExchange(Exchange):
|
|||||||
|
|
||||||
|
|
||||||
class DirectExchange(Exchange):
|
class DirectExchange(Exchange):
|
||||||
"""
|
"""Exchange where all topic keys are split, sending to second half.
|
||||||
Exchange where all topic keys are split, sending to second half.
|
|
||||||
i.e. "compute.host" sends a message to "compute.host" running on "host"
|
i.e. "compute.host" sends a message to "compute.host" running on "host"
|
||||||
"""
|
"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@ -388,20 +304,9 @@ class DirectExchange(Exchange):
|
|||||||
return [(key, e)]
|
return [(key, e)]
|
||||||
|
|
||||||
|
|
||||||
class MatchMakerRing(MatchMakerBase):
|
|
||||||
"""
|
|
||||||
Match Maker where hosts are loaded from a static hashmap.
|
|
||||||
"""
|
|
||||||
def __init__(self, ring=None):
|
|
||||||
super(MatchMakerRing, self).__init__()
|
|
||||||
self.add_binding(FanoutBinding(), FanoutRingExchange(ring))
|
|
||||||
self.add_binding(DirectBinding(), DirectExchange())
|
|
||||||
self.add_binding(TopicBinding(), RoundRobinRingExchange(ring))
|
|
||||||
|
|
||||||
|
|
||||||
class MatchMakerLocalhost(MatchMakerBase):
|
class MatchMakerLocalhost(MatchMakerBase):
|
||||||
"""
|
"""Match Maker where all bare topics resolve to localhost.
|
||||||
Match Maker where all bare topics resolve to localhost.
|
|
||||||
Useful for testing.
|
Useful for testing.
|
||||||
"""
|
"""
|
||||||
def __init__(self, host='localhost'):
|
def __init__(self, host='localhost'):
|
||||||
@ -412,13 +317,13 @@ class MatchMakerLocalhost(MatchMakerBase):
|
|||||||
|
|
||||||
|
|
||||||
class MatchMakerStub(MatchMakerBase):
|
class MatchMakerStub(MatchMakerBase):
|
||||||
"""
|
"""Match Maker where topics are untouched.
|
||||||
Match Maker where topics are untouched.
|
|
||||||
Useful for testing, or for AMQP/brokered queues.
|
Useful for testing, or for AMQP/brokered queues.
|
||||||
Will not work where knowledge of hosts is known (i.e. zeromq)
|
Will not work where knowledge of hosts is known (i.e. zeromq)
|
||||||
"""
|
"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(MatchMakerLocalhost, self).__init__()
|
super(MatchMakerStub, self).__init__()
|
||||||
|
|
||||||
self.add_binding(FanoutBinding(), StubExchange())
|
self.add_binding(FanoutBinding(), StubExchange())
|
||||||
self.add_binding(DirectBinding(), StubExchange())
|
self.add_binding(DirectBinding(), StubExchange())
|
||||||
|
@ -55,8 +55,8 @@ class RedisExchange(mm_common.Exchange):
|
|||||||
|
|
||||||
|
|
||||||
class RedisTopicExchange(RedisExchange):
|
class RedisTopicExchange(RedisExchange):
|
||||||
"""
|
"""Exchange where all topic keys are split, sending to second half.
|
||||||
Exchange where all topic keys are split, sending to second half.
|
|
||||||
i.e. "compute.host" sends a message to "compute" running on "host"
|
i.e. "compute.host" sends a message to "compute" running on "host"
|
||||||
"""
|
"""
|
||||||
def run(self, topic):
|
def run(self, topic):
|
||||||
@ -77,9 +77,7 @@ class RedisTopicExchange(RedisExchange):
|
|||||||
|
|
||||||
|
|
||||||
class RedisFanoutExchange(RedisExchange):
|
class RedisFanoutExchange(RedisExchange):
|
||||||
"""
|
"""Return a list of all hosts."""
|
||||||
Return a list of all hosts.
|
|
||||||
"""
|
|
||||||
def run(self, topic):
|
def run(self, topic):
|
||||||
topic = topic.split('~', 1)[1]
|
topic = topic.split('~', 1)[1]
|
||||||
hosts = self.redis.smembers(topic)
|
hosts = self.redis.smembers(topic)
|
||||||
@ -90,9 +88,7 @@ class RedisFanoutExchange(RedisExchange):
|
|||||||
|
|
||||||
|
|
||||||
class MatchMakerRedis(mm_common.HeartbeatMatchMakerBase):
|
class MatchMakerRedis(mm_common.HeartbeatMatchMakerBase):
|
||||||
"""
|
"""MatchMaker registering and looking-up hosts with a Redis server."""
|
||||||
MatchMaker registering and looking-up hosts with a Redis server.
|
|
||||||
"""
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(MatchMakerRedis, self).__init__()
|
super(MatchMakerRedis, self).__init__()
|
||||||
|
|
||||||
|
110
trove/openstack/common/rpc/matchmaker_ring.py
Normal file
110
trove/openstack/common/rpc/matchmaker_ring.py
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011-2013 Cloudscaling Group, 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.
|
||||||
|
"""
|
||||||
|
The MatchMaker classes should except a Topic or Fanout exchange key and
|
||||||
|
return keys for direct exchanges, per (approximate) AMQP parlance.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import itertools
|
||||||
|
import json
|
||||||
|
|
||||||
|
from oslo.config import cfg
|
||||||
|
|
||||||
|
from trove.openstack.common.gettextutils import _
|
||||||
|
from trove.openstack.common import log as logging
|
||||||
|
from trove.openstack.common.rpc import matchmaker as mm
|
||||||
|
|
||||||
|
|
||||||
|
matchmaker_opts = [
|
||||||
|
# Matchmaker ring file
|
||||||
|
cfg.StrOpt('ringfile',
|
||||||
|
deprecated_name='matchmaker_ringfile',
|
||||||
|
deprecated_group='DEFAULT',
|
||||||
|
default='/etc/oslo/matchmaker_ring.json',
|
||||||
|
help='Matchmaker ring file (JSON)'),
|
||||||
|
]
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
CONF.register_opts(matchmaker_opts, 'matchmaker_ring')
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class RingExchange(mm.Exchange):
|
||||||
|
"""Match Maker where hosts are loaded from a static JSON formatted file.
|
||||||
|
|
||||||
|
__init__ takes optional ring dictionary argument, otherwise
|
||||||
|
loads the ringfile from CONF.mathcmaker_ringfile.
|
||||||
|
"""
|
||||||
|
def __init__(self, ring=None):
|
||||||
|
super(RingExchange, self).__init__()
|
||||||
|
|
||||||
|
if ring:
|
||||||
|
self.ring = ring
|
||||||
|
else:
|
||||||
|
fh = open(CONF.matchmaker_ring.ringfile, 'r')
|
||||||
|
self.ring = json.load(fh)
|
||||||
|
fh.close()
|
||||||
|
|
||||||
|
self.ring0 = {}
|
||||||
|
for k in self.ring.keys():
|
||||||
|
self.ring0[k] = itertools.cycle(self.ring[k])
|
||||||
|
|
||||||
|
def _ring_has(self, key):
|
||||||
|
if key in self.ring0:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class RoundRobinRingExchange(RingExchange):
|
||||||
|
"""A Topic Exchange based on a hashmap."""
|
||||||
|
def __init__(self, ring=None):
|
||||||
|
super(RoundRobinRingExchange, self).__init__(ring)
|
||||||
|
|
||||||
|
def run(self, key):
|
||||||
|
if not self._ring_has(key):
|
||||||
|
LOG.warn(
|
||||||
|
_("No key defining hosts for topic '%s', "
|
||||||
|
"see ringfile") % (key, )
|
||||||
|
)
|
||||||
|
return []
|
||||||
|
host = next(self.ring0[key])
|
||||||
|
return [(key + '.' + host, host)]
|
||||||
|
|
||||||
|
|
||||||
|
class FanoutRingExchange(RingExchange):
|
||||||
|
"""Fanout Exchange based on a hashmap."""
|
||||||
|
def __init__(self, ring=None):
|
||||||
|
super(FanoutRingExchange, self).__init__(ring)
|
||||||
|
|
||||||
|
def run(self, key):
|
||||||
|
# Assume starts with "fanout~", strip it for lookup.
|
||||||
|
nkey = key.split('fanout~')[1:][0]
|
||||||
|
if not self._ring_has(nkey):
|
||||||
|
LOG.warn(
|
||||||
|
_("No key defining hosts for topic '%s', "
|
||||||
|
"see ringfile") % (nkey, )
|
||||||
|
)
|
||||||
|
return []
|
||||||
|
return map(lambda x: (key + '.' + x, x), self.ring[nkey])
|
||||||
|
|
||||||
|
|
||||||
|
class MatchMakerRing(mm.MatchMakerBase):
|
||||||
|
"""Match Maker where hosts are loaded from a static hashmap."""
|
||||||
|
def __init__(self, ring=None):
|
||||||
|
super(MatchMakerRing, self).__init__()
|
||||||
|
self.add_binding(mm.FanoutBinding(), FanoutRingExchange(ring))
|
||||||
|
self.add_binding(mm.DirectBinding(), mm.DirectExchange())
|
||||||
|
self.add_binding(mm.TopicBinding(), RoundRobinRingExchange(ring))
|
@ -1,6 +1,6 @@
|
|||||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
# Copyright 2012 Red Hat, Inc.
|
# Copyright 2012-2013 Red Hat, Inc.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
# 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
|
# not use this file except in compliance with the License. You may obtain
|
||||||
@ -23,6 +23,8 @@ For more information about rpc API version numbers, see:
|
|||||||
|
|
||||||
|
|
||||||
from trove.openstack.common import rpc
|
from trove.openstack.common import rpc
|
||||||
|
from trove.openstack.common.rpc import common as rpc_common
|
||||||
|
from trove.openstack.common.rpc import serializer as rpc_serializer
|
||||||
|
|
||||||
|
|
||||||
class RpcProxy(object):
|
class RpcProxy(object):
|
||||||
@ -34,16 +36,28 @@ class RpcProxy(object):
|
|||||||
rpc API.
|
rpc API.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, topic, default_version):
|
# The default namespace, which can be overriden in a subclass.
|
||||||
|
RPC_API_NAMESPACE = None
|
||||||
|
|
||||||
|
def __init__(self, topic, default_version, version_cap=None,
|
||||||
|
serializer=None):
|
||||||
"""Initialize an RpcProxy.
|
"""Initialize an RpcProxy.
|
||||||
|
|
||||||
:param topic: The topic to use for all messages.
|
:param topic: The topic to use for all messages.
|
||||||
:param default_version: The default API version to request in all
|
:param default_version: The default API version to request in all
|
||||||
outgoing messages. This can be overridden on a per-message
|
outgoing messages. This can be overridden on a per-message
|
||||||
basis.
|
basis.
|
||||||
|
:param version_cap: Optionally cap the maximum version used for sent
|
||||||
|
messages.
|
||||||
|
:param serializer: Optionaly (de-)serialize entities with a
|
||||||
|
provided helper.
|
||||||
"""
|
"""
|
||||||
self.topic = topic
|
self.topic = topic
|
||||||
self.default_version = default_version
|
self.default_version = default_version
|
||||||
|
self.version_cap = version_cap
|
||||||
|
if serializer is None:
|
||||||
|
serializer = rpc_serializer.NoOpSerializer()
|
||||||
|
self.serializer = serializer
|
||||||
super(RpcProxy, self).__init__()
|
super(RpcProxy, self).__init__()
|
||||||
|
|
||||||
def _set_version(self, msg, vers):
|
def _set_version(self, msg, vers):
|
||||||
@ -52,19 +66,44 @@ class RpcProxy(object):
|
|||||||
:param msg: The message having a version added to it.
|
:param msg: The message having a version added to it.
|
||||||
:param vers: The version number to add to the message.
|
:param vers: The version number to add to the message.
|
||||||
"""
|
"""
|
||||||
msg['version'] = vers if vers else self.default_version
|
v = vers if vers else self.default_version
|
||||||
|
if (self.version_cap and not
|
||||||
|
rpc_common.version_is_compatible(self.version_cap, v)):
|
||||||
|
raise rpc_common.RpcVersionCapError(version=self.version_cap)
|
||||||
|
msg['version'] = v
|
||||||
|
|
||||||
def _get_topic(self, topic):
|
def _get_topic(self, topic):
|
||||||
"""Return the topic to use for a message."""
|
"""Return the topic to use for a message."""
|
||||||
return topic if topic else self.topic
|
return topic if topic else self.topic
|
||||||
|
|
||||||
|
def can_send_version(self, version):
|
||||||
|
"""Check to see if a version is compatible with the version cap."""
|
||||||
|
return (not self.version_cap or
|
||||||
|
rpc_common.version_is_compatible(self.version_cap, version))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def make_namespaced_msg(method, namespace, **kwargs):
|
def make_namespaced_msg(method, namespace, **kwargs):
|
||||||
return {'method': method, 'namespace': namespace, 'args': kwargs}
|
return {'method': method, 'namespace': namespace, 'args': kwargs}
|
||||||
|
|
||||||
@staticmethod
|
def make_msg(self, method, **kwargs):
|
||||||
def make_msg(method, **kwargs):
|
return self.make_namespaced_msg(method, self.RPC_API_NAMESPACE,
|
||||||
return RpcProxy.make_namespaced_msg(method, None, **kwargs)
|
**kwargs)
|
||||||
|
|
||||||
|
def _serialize_msg_args(self, context, kwargs):
|
||||||
|
"""Helper method called to serialize message arguments.
|
||||||
|
|
||||||
|
This calls our serializer on each argument, returning a new
|
||||||
|
set of args that have been serialized.
|
||||||
|
|
||||||
|
:param context: The request context
|
||||||
|
:param kwargs: The arguments to serialize
|
||||||
|
:returns: A new set of serialized arguments
|
||||||
|
"""
|
||||||
|
new_kwargs = dict()
|
||||||
|
for argname, arg in kwargs.iteritems():
|
||||||
|
new_kwargs[argname] = self.serializer.serialize_entity(context,
|
||||||
|
arg)
|
||||||
|
return new_kwargs
|
||||||
|
|
||||||
def call(self, context, msg, topic=None, version=None, timeout=None):
|
def call(self, context, msg, topic=None, version=None, timeout=None):
|
||||||
"""rpc.call() a remote method.
|
"""rpc.call() a remote method.
|
||||||
@ -81,9 +120,11 @@ class RpcProxy(object):
|
|||||||
:returns: The return value from the remote method.
|
:returns: The return value from the remote method.
|
||||||
"""
|
"""
|
||||||
self._set_version(msg, version)
|
self._set_version(msg, version)
|
||||||
|
msg['args'] = self._serialize_msg_args(context, msg['args'])
|
||||||
real_topic = self._get_topic(topic)
|
real_topic = self._get_topic(topic)
|
||||||
try:
|
try:
|
||||||
return rpc.call(context, real_topic, msg, timeout)
|
result = rpc.call(context, real_topic, msg, timeout)
|
||||||
|
return self.serializer.deserialize_entity(context, result)
|
||||||
except rpc.common.Timeout as exc:
|
except rpc.common.Timeout as exc:
|
||||||
raise rpc.common.Timeout(
|
raise rpc.common.Timeout(
|
||||||
exc.info, real_topic, msg.get('method'))
|
exc.info, real_topic, msg.get('method'))
|
||||||
@ -104,9 +145,11 @@ class RpcProxy(object):
|
|||||||
from the remote method as they arrive.
|
from the remote method as they arrive.
|
||||||
"""
|
"""
|
||||||
self._set_version(msg, version)
|
self._set_version(msg, version)
|
||||||
|
msg['args'] = self._serialize_msg_args(context, msg['args'])
|
||||||
real_topic = self._get_topic(topic)
|
real_topic = self._get_topic(topic)
|
||||||
try:
|
try:
|
||||||
return rpc.multicall(context, real_topic, msg, timeout)
|
result = rpc.multicall(context, real_topic, msg, timeout)
|
||||||
|
return self.serializer.deserialize_entity(context, result)
|
||||||
except rpc.common.Timeout as exc:
|
except rpc.common.Timeout as exc:
|
||||||
raise rpc.common.Timeout(
|
raise rpc.common.Timeout(
|
||||||
exc.info, real_topic, msg.get('method'))
|
exc.info, real_topic, msg.get('method'))
|
||||||
@ -124,6 +167,7 @@ class RpcProxy(object):
|
|||||||
remote method.
|
remote method.
|
||||||
"""
|
"""
|
||||||
self._set_version(msg, version)
|
self._set_version(msg, version)
|
||||||
|
msg['args'] = self._serialize_msg_args(context, msg['args'])
|
||||||
rpc.cast(context, self._get_topic(topic), msg)
|
rpc.cast(context, self._get_topic(topic), msg)
|
||||||
|
|
||||||
def fanout_cast(self, context, msg, topic=None, version=None):
|
def fanout_cast(self, context, msg, topic=None, version=None):
|
||||||
@ -139,6 +183,7 @@ class RpcProxy(object):
|
|||||||
from the remote method.
|
from the remote method.
|
||||||
"""
|
"""
|
||||||
self._set_version(msg, version)
|
self._set_version(msg, version)
|
||||||
|
msg['args'] = self._serialize_msg_args(context, msg['args'])
|
||||||
rpc.fanout_cast(context, self._get_topic(topic), msg)
|
rpc.fanout_cast(context, self._get_topic(topic), msg)
|
||||||
|
|
||||||
def cast_to_server(self, context, server_params, msg, topic=None,
|
def cast_to_server(self, context, server_params, msg, topic=None,
|
||||||
@ -157,6 +202,7 @@ class RpcProxy(object):
|
|||||||
return values.
|
return values.
|
||||||
"""
|
"""
|
||||||
self._set_version(msg, version)
|
self._set_version(msg, version)
|
||||||
|
msg['args'] = self._serialize_msg_args(context, msg['args'])
|
||||||
rpc.cast_to_server(context, server_params, self._get_topic(topic), msg)
|
rpc.cast_to_server(context, server_params, self._get_topic(topic), msg)
|
||||||
|
|
||||||
def fanout_cast_to_server(self, context, server_params, msg, topic=None,
|
def fanout_cast_to_server(self, context, server_params, msg, topic=None,
|
||||||
@ -175,5 +221,6 @@ class RpcProxy(object):
|
|||||||
return values.
|
return values.
|
||||||
"""
|
"""
|
||||||
self._set_version(msg, version)
|
self._set_version(msg, version)
|
||||||
|
msg['args'] = self._serialize_msg_args(context, msg['args'])
|
||||||
rpc.fanout_cast_to_server(context, server_params,
|
rpc.fanout_cast_to_server(context, server_params,
|
||||||
self._get_topic(topic), msg)
|
self._get_topic(topic), msg)
|
||||||
|
52
trove/openstack/common/rpc/serializer.py
Normal file
52
trove/openstack/common/rpc/serializer.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
# Copyright 2013 IBM Corp.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""Provides the definition of an RPC serialization handler"""
|
||||||
|
|
||||||
|
import abc
|
||||||
|
|
||||||
|
|
||||||
|
class Serializer(object):
|
||||||
|
"""Generic (de-)serialization definition base class."""
|
||||||
|
__metaclass__ = abc.ABCMeta
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def serialize_entity(self, context, entity):
|
||||||
|
"""Serialize something to primitive form.
|
||||||
|
|
||||||
|
:param context: Security context
|
||||||
|
:param entity: Entity to be serialized
|
||||||
|
:returns: Serialized form of entity
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def deserialize_entity(self, context, entity):
|
||||||
|
"""Deserialize something from primitive form.
|
||||||
|
|
||||||
|
:param context: Security context
|
||||||
|
:param entity: Primitive to be deserialized
|
||||||
|
:returns: Deserialized form of entity
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NoOpSerializer(Serializer):
|
||||||
|
"""A serializer that does nothing."""
|
||||||
|
|
||||||
|
def serialize_entity(self, context, entity):
|
||||||
|
return entity
|
||||||
|
|
||||||
|
def deserialize_entity(self, context, entity):
|
||||||
|
return entity
|
@ -30,7 +30,8 @@ LOG = logging.getLogger(__name__)
|
|||||||
class Service(service.Service):
|
class Service(service.Service):
|
||||||
"""Service object for binaries running on hosts.
|
"""Service object for binaries running on hosts.
|
||||||
|
|
||||||
A service enables rpc by listening to queues based on topic and host."""
|
A service enables rpc by listening to queues based on topic and host.
|
||||||
|
"""
|
||||||
def __init__(self, host, topic, manager=None):
|
def __init__(self, host, topic, manager=None):
|
||||||
super(Service, self).__init__()
|
super(Service, self).__init__()
|
||||||
self.host = host
|
self.host = host
|
||||||
|
@ -27,6 +27,7 @@ import sys
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
import eventlet
|
import eventlet
|
||||||
|
from eventlet import event
|
||||||
import logging as std_logging
|
import logging as std_logging
|
||||||
from oslo.config import cfg
|
from oslo.config import cfg
|
||||||
|
|
||||||
@ -51,19 +52,8 @@ class Launcher(object):
|
|||||||
:returns: None
|
:returns: None
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self._services = threadgroup.ThreadGroup()
|
self.services = Services()
|
||||||
eventlet_backdoor.initialize_if_enabled()
|
self.backdoor_port = eventlet_backdoor.initialize_if_enabled()
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def run_service(service):
|
|
||||||
"""Start and wait for a service to finish.
|
|
||||||
|
|
||||||
:param service: service to run and wait for.
|
|
||||||
:returns: None
|
|
||||||
|
|
||||||
"""
|
|
||||||
service.start()
|
|
||||||
service.wait()
|
|
||||||
|
|
||||||
def launch_service(self, service):
|
def launch_service(self, service):
|
||||||
"""Load and start the given service.
|
"""Load and start the given service.
|
||||||
@ -72,7 +62,8 @@ class Launcher(object):
|
|||||||
:returns: None
|
:returns: None
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self._services.add_thread(self.run_service, service)
|
service.backdoor_port = self.backdoor_port
|
||||||
|
self.services.add(service)
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
"""Stop all services which are currently running.
|
"""Stop all services which are currently running.
|
||||||
@ -80,7 +71,7 @@ class Launcher(object):
|
|||||||
:returns: None
|
:returns: None
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self._services.stop()
|
self.services.stop()
|
||||||
|
|
||||||
def wait(self):
|
def wait(self):
|
||||||
"""Waits until all services have been stopped, and then returns.
|
"""Waits until all services have been stopped, and then returns.
|
||||||
@ -88,7 +79,7 @@ class Launcher(object):
|
|||||||
:returns: None
|
:returns: None
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self._services.wait()
|
self.services.wait()
|
||||||
|
|
||||||
|
|
||||||
class SignalExit(SystemExit):
|
class SignalExit(SystemExit):
|
||||||
@ -123,9 +114,9 @@ class ServiceLauncher(Launcher):
|
|||||||
except SystemExit as exc:
|
except SystemExit as exc:
|
||||||
status = exc.code
|
status = exc.code
|
||||||
finally:
|
finally:
|
||||||
|
self.stop()
|
||||||
if rpc:
|
if rpc:
|
||||||
rpc.cleanup()
|
rpc.cleanup()
|
||||||
self.stop()
|
|
||||||
return status
|
return status
|
||||||
|
|
||||||
|
|
||||||
@ -188,7 +179,8 @@ class ProcessLauncher(object):
|
|||||||
random.seed()
|
random.seed()
|
||||||
|
|
||||||
launcher = Launcher()
|
launcher = Launcher()
|
||||||
launcher.run_service(service)
|
launcher.launch_service(service)
|
||||||
|
launcher.wait()
|
||||||
|
|
||||||
def _start_child(self, wrap):
|
def _start_child(self, wrap):
|
||||||
if len(wrap.forktimes) > wrap.workers:
|
if len(wrap.forktimes) > wrap.workers:
|
||||||
@ -270,7 +262,7 @@ class ProcessLauncher(object):
|
|||||||
return wrap
|
return wrap
|
||||||
|
|
||||||
def wait(self):
|
def wait(self):
|
||||||
"""Loop waiting on children to die and respawning as necessary"""
|
"""Loop waiting on children to die and respawning as necessary."""
|
||||||
|
|
||||||
LOG.debug(_('Full set of CONF:'))
|
LOG.debug(_('Full set of CONF:'))
|
||||||
CONF.log_opt_values(LOG, std_logging.DEBUG)
|
CONF.log_opt_values(LOG, std_logging.DEBUG)
|
||||||
@ -312,15 +304,60 @@ class Service(object):
|
|||||||
def __init__(self, threads=1000):
|
def __init__(self, threads=1000):
|
||||||
self.tg = threadgroup.ThreadGroup(threads)
|
self.tg = threadgroup.ThreadGroup(threads)
|
||||||
|
|
||||||
|
# signal that the service is done shutting itself down:
|
||||||
|
self._done = event.Event()
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self.tg.stop()
|
self.tg.stop()
|
||||||
|
self.tg.wait()
|
||||||
|
self._done.send()
|
||||||
|
|
||||||
|
def wait(self):
|
||||||
|
self._done.wait()
|
||||||
|
|
||||||
|
|
||||||
|
class Services(object):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.services = []
|
||||||
|
self.tg = threadgroup.ThreadGroup()
|
||||||
|
self.done = event.Event()
|
||||||
|
|
||||||
|
def add(self, service):
|
||||||
|
self.services.append(service)
|
||||||
|
self.tg.add_thread(self.run_service, service, self.done)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
# wait for graceful shutdown of services:
|
||||||
|
for service in self.services:
|
||||||
|
service.stop()
|
||||||
|
service.wait()
|
||||||
|
|
||||||
|
# each service has performed cleanup, now signal that the run_service
|
||||||
|
# wrapper threads can now die:
|
||||||
|
self.done.send()
|
||||||
|
|
||||||
|
# reap threads:
|
||||||
|
self.tg.stop()
|
||||||
|
|
||||||
def wait(self):
|
def wait(self):
|
||||||
self.tg.wait()
|
self.tg.wait()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def run_service(service, done):
|
||||||
|
"""Service start wrapper.
|
||||||
|
|
||||||
|
:param service: service to run
|
||||||
|
:param done: event to wait on until a shutdown is triggered
|
||||||
|
:returns: None
|
||||||
|
|
||||||
|
"""
|
||||||
|
service.start()
|
||||||
|
done.wait()
|
||||||
|
|
||||||
|
|
||||||
def launch(service, workers=None):
|
def launch(service, workers=None):
|
||||||
if workers:
|
if workers:
|
||||||
|
@ -26,7 +26,7 @@ LOG = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
def _thread_done(gt, *args, **kwargs):
|
def _thread_done(gt, *args, **kwargs):
|
||||||
""" Callback function to be passed to GreenThread.link() when we spawn()
|
"""Callback function to be passed to GreenThread.link() when we spawn()
|
||||||
Calls the :class:`ThreadGroup` to notify if.
|
Calls the :class:`ThreadGroup` to notify if.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@ -34,7 +34,7 @@ def _thread_done(gt, *args, **kwargs):
|
|||||||
|
|
||||||
|
|
||||||
class Thread(object):
|
class Thread(object):
|
||||||
""" Wrapper around a greenthread, that holds a reference to the
|
"""Wrapper around a greenthread, that holds a reference to the
|
||||||
:class:`ThreadGroup`. The Thread will notify the :class:`ThreadGroup` when
|
:class:`ThreadGroup`. The Thread will notify the :class:`ThreadGroup` when
|
||||||
it has done so it can be removed from the threads list.
|
it has done so it can be removed from the threads list.
|
||||||
"""
|
"""
|
||||||
@ -50,7 +50,7 @@ class Thread(object):
|
|||||||
|
|
||||||
|
|
||||||
class ThreadGroup(object):
|
class ThreadGroup(object):
|
||||||
""" The point of the ThreadGroup classis to:
|
"""The point of the ThreadGroup classis to:
|
||||||
|
|
||||||
* keep track of timers and greenthreads (making it easier to stop them
|
* keep track of timers and greenthreads (making it easier to stop them
|
||||||
when need be).
|
when need be).
|
||||||
@ -61,6 +61,13 @@ class ThreadGroup(object):
|
|||||||
self.threads = []
|
self.threads = []
|
||||||
self.timers = []
|
self.timers = []
|
||||||
|
|
||||||
|
def add_dynamic_timer(self, callback, initial_delay=None,
|
||||||
|
periodic_interval_max=None, *args, **kwargs):
|
||||||
|
timer = loopingcall.DynamicLoopingCall(callback, *args, **kwargs)
|
||||||
|
timer.start(initial_delay=initial_delay,
|
||||||
|
periodic_interval_max=periodic_interval_max)
|
||||||
|
self.timers.append(timer)
|
||||||
|
|
||||||
def add_timer(self, interval, callback, initial_delay=None,
|
def add_timer(self, interval, callback, initial_delay=None,
|
||||||
*args, **kwargs):
|
*args, **kwargs):
|
||||||
pulse = loopingcall.FixedIntervalLoopingCall(callback, *args, **kwargs)
|
pulse = loopingcall.FixedIntervalLoopingCall(callback, *args, **kwargs)
|
||||||
|
@ -23,6 +23,7 @@ import calendar
|
|||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
import iso8601
|
import iso8601
|
||||||
|
import six
|
||||||
|
|
||||||
|
|
||||||
# ISO 8601 extended time format with microseconds
|
# ISO 8601 extended time format with microseconds
|
||||||
@ -32,7 +33,7 @@ PERFECT_TIME_FORMAT = _ISO8601_TIME_FORMAT_SUBSECOND
|
|||||||
|
|
||||||
|
|
||||||
def isotime(at=None, subsecond=False):
|
def isotime(at=None, subsecond=False):
|
||||||
"""Stringify time in ISO 8601 format"""
|
"""Stringify time in ISO 8601 format."""
|
||||||
if not at:
|
if not at:
|
||||||
at = utcnow()
|
at = utcnow()
|
||||||
st = at.strftime(_ISO8601_TIME_FORMAT
|
st = at.strftime(_ISO8601_TIME_FORMAT
|
||||||
@ -44,7 +45,7 @@ def isotime(at=None, subsecond=False):
|
|||||||
|
|
||||||
|
|
||||||
def parse_isotime(timestr):
|
def parse_isotime(timestr):
|
||||||
"""Parse time from ISO 8601 format"""
|
"""Parse time from ISO 8601 format."""
|
||||||
try:
|
try:
|
||||||
return iso8601.parse_date(timestr)
|
return iso8601.parse_date(timestr)
|
||||||
except iso8601.ParseError as e:
|
except iso8601.ParseError as e:
|
||||||
@ -66,7 +67,7 @@ def parse_strtime(timestr, fmt=PERFECT_TIME_FORMAT):
|
|||||||
|
|
||||||
|
|
||||||
def normalize_time(timestamp):
|
def normalize_time(timestamp):
|
||||||
"""Normalize time in arbitrary timezone to UTC naive object"""
|
"""Normalize time in arbitrary timezone to UTC naive object."""
|
||||||
offset = timestamp.utcoffset()
|
offset = timestamp.utcoffset()
|
||||||
if offset is None:
|
if offset is None:
|
||||||
return timestamp
|
return timestamp
|
||||||
@ -75,14 +76,14 @@ def normalize_time(timestamp):
|
|||||||
|
|
||||||
def is_older_than(before, seconds):
|
def is_older_than(before, seconds):
|
||||||
"""Return True if before is older than seconds."""
|
"""Return True if before is older than seconds."""
|
||||||
if isinstance(before, basestring):
|
if isinstance(before, six.string_types):
|
||||||
before = parse_strtime(before).replace(tzinfo=None)
|
before = parse_strtime(before).replace(tzinfo=None)
|
||||||
return utcnow() - before > datetime.timedelta(seconds=seconds)
|
return utcnow() - before > datetime.timedelta(seconds=seconds)
|
||||||
|
|
||||||
|
|
||||||
def is_newer_than(after, seconds):
|
def is_newer_than(after, seconds):
|
||||||
"""Return True if after is newer than seconds."""
|
"""Return True if after is newer than seconds."""
|
||||||
if isinstance(after, basestring):
|
if isinstance(after, six.string_types):
|
||||||
after = parse_strtime(after).replace(tzinfo=None)
|
after = parse_strtime(after).replace(tzinfo=None)
|
||||||
return after - utcnow() > datetime.timedelta(seconds=seconds)
|
return after - utcnow() > datetime.timedelta(seconds=seconds)
|
||||||
|
|
||||||
@ -103,7 +104,7 @@ def utcnow():
|
|||||||
|
|
||||||
|
|
||||||
def iso8601_from_timestamp(timestamp):
|
def iso8601_from_timestamp(timestamp):
|
||||||
"""Returns a iso8601 formated date from timestamp"""
|
"""Returns a iso8601 formated date from timestamp."""
|
||||||
return isotime(datetime.datetime.utcfromtimestamp(timestamp))
|
return isotime(datetime.datetime.utcfromtimestamp(timestamp))
|
||||||
|
|
||||||
|
|
||||||
@ -111,9 +112,9 @@ utcnow.override_time = None
|
|||||||
|
|
||||||
|
|
||||||
def set_time_override(override_time=datetime.datetime.utcnow()):
|
def set_time_override(override_time=datetime.datetime.utcnow()):
|
||||||
"""
|
"""Overrides utils.utcnow.
|
||||||
Override utils.utcnow to return a constant time or a list thereof,
|
|
||||||
one at a time.
|
Make it return a constant time or a list thereof, one at a time.
|
||||||
"""
|
"""
|
||||||
utcnow.override_time = override_time
|
utcnow.override_time = override_time
|
||||||
|
|
||||||
@ -141,7 +142,8 @@ def clear_time_override():
|
|||||||
def marshall_now(now=None):
|
def marshall_now(now=None):
|
||||||
"""Make an rpc-safe datetime with microseconds.
|
"""Make an rpc-safe datetime with microseconds.
|
||||||
|
|
||||||
Note: tzinfo is stripped, but not required for relative times."""
|
Note: tzinfo is stripped, but not required for relative times.
|
||||||
|
"""
|
||||||
if not now:
|
if not now:
|
||||||
now = utcnow()
|
now = utcnow()
|
||||||
return dict(day=now.day, month=now.month, year=now.year, hour=now.hour,
|
return dict(day=now.day, month=now.month, year=now.year, hour=now.hour,
|
||||||
@ -161,7 +163,8 @@ def unmarshall_time(tyme):
|
|||||||
|
|
||||||
|
|
||||||
def delta_seconds(before, after):
|
def delta_seconds(before, after):
|
||||||
"""
|
"""Return the difference between two timing objects.
|
||||||
|
|
||||||
Compute the difference in seconds between two date, time, or
|
Compute the difference in seconds between two date, time, or
|
||||||
datetime objects (as a float, to microsecond resolution).
|
datetime objects (as a float, to microsecond resolution).
|
||||||
"""
|
"""
|
||||||
@ -174,8 +177,7 @@ def delta_seconds(before, after):
|
|||||||
|
|
||||||
|
|
||||||
def is_soon(dt, window):
|
def is_soon(dt, window):
|
||||||
"""
|
"""Determines if time is going to happen in the next window seconds.
|
||||||
Determines if time is going to happen in the next window seconds.
|
|
||||||
|
|
||||||
:params dt: the time
|
:params dt: the time
|
||||||
:params window: minimum seconds to remain to consider the time not soon
|
:params window: minimum seconds to remain to consider the time not soon
|
||||||
|
Loading…
Reference in New Issue
Block a user