From 6fceab1214e82303e50ae78c7512180915c0e6cf Mon Sep 17 00:00:00 2001 From: Davanum Srinivas Date: Mon, 19 Oct 2015 19:37:19 -0400 Subject: [PATCH 01/16] bootstrap branch Change-Id: I22bb41192fa8d99dd47fcae49e6711cd1b5c84d7 --- .gitreview | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitreview b/.gitreview index beb811ae3..24d3e8df3 100644 --- a/.gitreview +++ b/.gitreview @@ -2,3 +2,4 @@ host=review.openstack.org port=29418 project=openstack/oslo.messaging.git +branch=feature/pika From ad2f475955ca5ef2bc750b2e6a610e6adfca928a Mon Sep 17 00:00:00 2001 From: Dmitriy Ukhlov Date: Thu, 1 Oct 2015 18:53:17 +0300 Subject: [PATCH 02/16] Implements rabbit-pika driver In this patch new driver implementation added and registered in setup.cfg, integrated ith tox functional tests. Implements: bp rabbit-pika Depends-On: I7bda78820e657b1e97bf888d4065a917eb317cfb Change-Id: I40842a03ce73d171644c362e3abfca2990aca58a --- oslo_messaging/_drivers/impl_pika.py | 1114 ++++++++++++++++++++++++++ requirements.txt | 2 + setup-test-env-pika.sh | 32 + setup.cfg | 1 + tox.ini | 3 + 5 files changed, 1152 insertions(+) create mode 100644 oslo_messaging/_drivers/impl_pika.py create mode 100755 setup-test-env-pika.sh diff --git a/oslo_messaging/_drivers/impl_pika.py b/oslo_messaging/_drivers/impl_pika.py new file mode 100644 index 000000000..84aa6882b --- /dev/null +++ b/oslo_messaging/_drivers/impl_pika.py @@ -0,0 +1,1114 @@ +# Copyright 2011 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +import collections +from concurrent import futures + +import pika +from pika import adapters as pika_adapters +from pika import credentials as pika_credentials +from pika import exceptions as pika_exceptions +from pika import spec as pika_spec + +import pika_pool +import retrying + +import six +import sys +import threading +import time +import uuid + + +from oslo_config import cfg +from oslo_log import log as logging + +from oslo_messaging._drivers import common +from oslo_messaging import exceptions + +from oslo_serialization import jsonutils + + +LOG = logging.getLogger(__name__) + +pika_opts = [ + cfg.IntOpt('channel_max', default=None, + help='Maximum number of channels to allow'), + cfg.IntOpt('frame_max', default=None, + help='The maximum byte size for an AMQP frame'), + cfg.IntOpt('heartbeat_interval', default=None, + help='How often to send heartbeats'), + cfg.BoolOpt('ssl', default=None, + help='Enable SSL'), + cfg.DictOpt('ssl_options', default=None, + help='Arguments passed to ssl.wrap_socket'), + cfg.FloatOpt('socket_timeout', default=None, + help='Use for high latency networks'), +] + +pika_pool_opts = [ + cfg.IntOpt('pool_max_size', default=10, + help="Maximum number of connections to keep queued."), + cfg.IntOpt('pool_max_overflow', default=10, + help="Maximum number of connections to create above " + "`pool_max_size`."), + cfg.IntOpt('pool_timeout', default=30, + help="Default number of seconds to wait for a connections to " + "available"), + cfg.IntOpt('pool_recycle', default=None, + help="Lifetime of a connection (since creation) in seconds " + "or None for no recycling. Expired connections are " + "closed on acquire."), + cfg.IntOpt('pool_stale', default=None, + help="Threshold at which inactive (since release) connections " + "are considered stale in seconds or None for no " + "staleness. Stale connections are closed on acquire.") +] + +notification_opts = [ + cfg.BoolOpt('notification_persistence', default=False, + help="Persist notification messages."), + cfg.StrOpt('default_notification_exchange', + default="${control_exchange}_notification", + help="Exchange name for for sending notifications"), + cfg.IntOpt( + 'default_notification_retry_attempts', default=-1, + help="Reconnecting retry count in case of connectivity problem during " + "sending notification, -1 means infinite retry." + ), + cfg.FloatOpt( + 'notification_retry_delay', default=0.1, + help="Reconnecting retry delay in case of connectivity problem during " + "sending notification message" + ) +] + +rpc_opts = [ + cfg.IntOpt('rpc_queue_expiration', default=60, + help="Time to live for rpc queues without consumers in " + "seconds."), + cfg.StrOpt('default_rpc_exchange', default="${control_exchange}_rpc", + help="Exchange name for for sending RPC messages"), + cfg.StrOpt('rpc_reply_exchange', default="${control_exchange}_rpc_reply", + help="Exchange name for for receiving RPC replies"), + cfg.IntOpt( + 'rpc_reply_retry_attempts', default=3, + help="Reconnecting retry count in case of connectivity problem during " + "sending reply. -1 means infinite retry." + ), + cfg.FloatOpt( + 'rpc_reply_retry_delay', default=0.1, + help="Reconnecting retry delay in case of connectivity problem during " + "sending reply." + ), + cfg.IntOpt( + 'default_rpc_retry_attempts', default=0, + help="Reconnecting retry count in case of connectivity problem during " + "sending RPC message, -1 means infinite retry." + ), + cfg.FloatOpt( + 'rpc_retry_delay', default=0.1, + help="Reconnecting retry delay in case of connectivity problem during " + "sending RPC message" + ) +] + + +def _is_eventlet_monkey_patched(): + if 'eventlet.patcher' not in sys.modules: + return False + import eventlet.patcher + return eventlet.patcher.is_monkey_patched('thread') + + +class ExchangeNotFoundException(exceptions.MessageDeliveryFailure): + pass + + +class MessageRejectedException(exceptions.MessageDeliveryFailure): + pass + + +class RoutingException(exceptions.MessageDeliveryFailure): + pass + + +class ConnectionException(exceptions.MessagingException): + pass + + +class EstablishConnectionException(ConnectionException): + pass + + +class PooledConnectionWithConfirmations(pika_pool.Connection): + @property + def channel(self): + if self.fairy.channel is None: + self.fairy.channel = self.fairy.cxn.channel() + self.fairy.channel.confirm_delivery() + return self.fairy.channel + + +class PikaEngine(object): + def __init__(self, conf, url, default_exchange=None): + self.conf = conf + + self.default_rpc_exchange = ( + conf.oslo_messaging_pika.default_rpc_exchange if + conf.oslo_messaging_pika.default_rpc_exchange else + default_exchange + ) + self.rpc_reply_exchange = ( + conf.oslo_messaging_pika.rpc_reply_exchange if + conf.oslo_messaging_pika.rpc_reply_exchange else + default_exchange + ) + + self.default_notification_exchange = ( + conf.oslo_messaging_pika.default_notification_exchange if + conf.oslo_messaging_pika.default_notification_exchange else + default_exchange + ) + + self.notification_persistence = ( + conf.oslo_messaging_pika.notification_persistence + ) + + self.rpc_reply_retry_attempts = ( + conf.oslo_messaging_pika.rpc_reply_retry_attempts + ) + if self.rpc_reply_retry_attempts is None: + raise ValueError("rpc_reply_retry_attempts should be integer") + self.rpc_reply_retry_delay = ( + conf.oslo_messaging_pika.rpc_reply_retry_delay + ) + if (self.rpc_reply_retry_delay is None or + self.rpc_reply_retry_delay < 0): + raise ValueError("rpc_reply_retry_delay should be non-negative " + "integer") + + self.default_rpc_retry_attempts = ( + conf.oslo_messaging_pika.default_rpc_retry_attempts + ) + if self.default_rpc_retry_attempts is None: + raise ValueError("default_rpc_retry_attempts should be an integer") + self.rpc_retry_delay = ( + conf.oslo_messaging_pika.rpc_retry_delay + ) + if (self.rpc_retry_delay is None or + self.rpc_retry_delay < 0): + raise ValueError("rpc_retry_delay should be non-negative integer") + + self.default_notification_retry_attempts = ( + conf.oslo_messaging_pika.default_notification_retry_attempts + ) + if self.default_notification_retry_attempts is None: + raise ValueError("default_notification_retry_attempts should be " + "an integer") + self.notification_retry_delay = ( + conf.oslo_messaging_pika.notification_retry_delay + ) + if (self.notification_retry_delay is None or + self.notification_retry_delay < 0): + raise ValueError("notification_retry_delay should be non-negative " + "integer") + + # preparing poller for listening replies + self._reply_queue = None + + self._reply_listener = None + self._reply_waiting_future_list = [] + + self._reply_consumer_enabled = False + self._reply_consumer_thread_run_flag = True + self._reply_consumer_lock = threading.Lock() + self._puller_thread = None + + # initializing connection parameters for configured RabbitMQ hosts + self._pika_next_connection_num = 0 + common_pika_params = { + 'virtual_host': url.virtual_host, + 'channel_max': self.conf.oslo_messaging_pika.channel_max, + 'frame_max': self.conf.oslo_messaging_pika.frame_max, + 'heartbeat_interval': + self.conf.oslo_messaging_pika.heartbeat_interval, + 'ssl': self.conf.oslo_messaging_pika.ssl, + 'ssl_options': self.conf.oslo_messaging_pika.ssl_options, + 'socket_timeout': self.conf.oslo_messaging_pika.socket_timeout, + } + + self._pika_params_list = [] + self._create_connection_lock = threading.Lock() + + for transport_host in url.hosts: + pika_params = pika.ConnectionParameters( + host=transport_host.hostname, + port=transport_host.port, + credentials=pika_credentials.PlainCredentials( + transport_host.username, transport_host.password + ), + **common_pika_params + ) + self._pika_params_list.append(pika_params) + + # initializing 2 connection pools: 1st for connections without + # confirmations, 2nd - with confirmations + self.connection_pool = pika_pool.QueuedPool( + create=self.create_connection, + max_size=self.conf.oslo_messaging_pika.pool_max_size, + max_overflow=self.conf.oslo_messaging_pika.pool_max_overflow, + timeout=self.conf.oslo_messaging_pika.pool_timeout, + recycle=self.conf.oslo_messaging_pika.pool_recycle, + stale=self.conf.oslo_messaging_pika.pool_stale, + ) + + self.connection_with_confirmation_pool = pika_pool.QueuedPool( + create=self.create_connection, + max_size=self.conf.oslo_messaging_pika.pool_max_size, + max_overflow=self.conf.oslo_messaging_pika.pool_max_overflow, + timeout=self.conf.oslo_messaging_pika.pool_timeout, + recycle=self.conf.oslo_messaging_pika.pool_recycle, + stale=self.conf.oslo_messaging_pika.pool_stale, + ) + + self.connection_with_confirmation_pool.Connection = ( + PooledConnectionWithConfirmations + ) + + def create_connection(self): + """Create and return connection to any available host. + + :return: cerated connection + :raise: ConnectionException if all hosts are not reachable + """ + host_num = len(self._pika_params_list) + connection_attempts = host_num + while connection_attempts > 0: + with self._create_connection_lock: + try: + return self.create_host_connection( + self._pika_next_connection_num + ) + except pika_pool.Connection.connectivity_errors as e: + LOG.warn(str(e)) + connection_attempts -= 1 + continue + finally: + self._pika_next_connection_num += 1 + self._pika_next_connection_num %= host_num + raise EstablishConnectionException( + "Can not establish connection to any configured RabbitMQ host: " + + str(self._pika_params_list) + ) + + def create_host_connection(self, host_index): + """Create new connection to host #host_index + + :return: New connection + """ + return pika_adapters.BlockingConnection( + self._pika_params_list[host_index] + ) + + def declare_queue_binding(self, exchange, queue, routing_key, + exchange_type, queue_expiration, + queue_auto_delete, durable, + timeout=None): + if timeout is not None and timeout < 0: + raise exceptions.MessagingTimeout( + "Timeout for current operation was expired." + ) + try: + with self.connection_pool.acquire(timeout=timeout) as conn: + conn.channel.exchange_declare( + exchange, exchange_type, auto_delete=True, durable=durable + ) + arguments = {} + + if queue_expiration > 0: + arguments['x-expires'] = queue_expiration * 1000 + + conn.channel.queue_declare( + queue, auto_delete=queue_auto_delete, durable=durable, + arguments=arguments + ) + + conn.channel.queue_bind(queue, exchange, routing_key) + except pika_pool.Timeout as e: + raise exceptions.MessagingTimeout( + "Timeout for current operation was expired. {}.".format(str(e)) + ) + except pika_pool.Connection.connectivity_errors as e: + raise ConnectionException( + "Connectivity problem detected during declaring queue " + "binding: exchange:{}, queue: {}, routing_key: {}, " + "exchange_type: {}, queue_expiration: {}, queue_auto_delete: " + "{}, durable: {}. {}".format( + exchange, queue, routing_key, exchange_type, + queue_expiration, queue_auto_delete, durable, str(e) + ) + ) + + @staticmethod + def _do_publish(pool, exchange, routing_key, body, properties, + mandatory, expiration_time): + timeout = (None if expiration_time is None else + expiration_time - time.time()) + if timeout is not None and timeout < 0: + raise exceptions.MessagingTimeout( + "Timeout for current operation was expired." + ) + + try: + with pool.acquire(timeout=timeout) as conn: + if timeout is not None: + properties.expiration = str(int(timeout)) + conn.channel.publish( + exchange=exchange, + routing_key=routing_key, + body=body, + properties=properties, + mandatory=mandatory + ) + except pika_exceptions.NackError as e: + raise MessageRejectedException( + "Can not send message: [body: {}], properties: {}] to " + "target [exchange: {}, routing_key: {}]. {}".format( + body, properties, exchange, routing_key, str(e) + ) + ) + except pika_exceptions.UnroutableError as e: + raise RoutingException( + "Can not deliver message:[body:{}, properties: {}] to any" + "queue using target: [exchange:{}, " + "routing_key:{}]. {}".format( + body, properties, exchange, routing_key, str(e) + ) + ) + except pika_pool.Timeout as e: + raise exceptions.MessagingTimeout( + "Timeout for current operation was expired. {}".format(str(e)) + ) + except pika_pool.Connection.connectivity_errors as e: + if (isinstance(e, pika_exceptions.ChannelClosed) + and e.args and e.args[0] == 404): + raise ExchangeNotFoundException( + "Attempt to send message to not existing exchange " + "detected, message: [body:{}, properties: {}], target: " + "[exchange:{}, routing_key:{}]. {}".format( + body, properties, exchange, routing_key, str(e) + ) + ) + raise ConnectionException( + "Connectivity problem detected during sending the message: " + "[body:{}, properties: {}] to target: [exchange:{}, " + "routing_key:{}]. {}".format( + body, properties, exchange, routing_key, str(e) + ) + ) + + def publish(self, exchange, routing_key, body, properties, confirm, + mandatory, expiration_time, retrier): + pool = (self.connection_with_confirmation_pool if confirm else + self.connection_pool) + + LOG.debug( + "Sending message:[body:{}; properties: {}] to target: " + "[exchange:{}; routing_key:{}]".format( + body, properties, exchange, routing_key + ) + ) + + do_publish = (self._do_publish if retrier is None else + retrier(self._do_publish)) + + return do_publish(pool, exchange, routing_key, body, properties, + mandatory, expiration_time) + + def get_reply_q(self, timeout=None): + if self._reply_consumer_enabled: + return self._reply_queue + + with self._reply_consumer_lock: + if self._reply_consumer_enabled: + return self._reply_queue + + if self._reply_queue is None: + self._reply_queue = "reply.{}.{}.{}".format( + self.conf.project, self.conf.prog, uuid.uuid4().hex + ) + + if self._reply_listener is None: + self._reply_listener = RpcReplyPikaListener( + pika_engine=self, + exchange=self.rpc_reply_exchange, + queue=self._reply_queue, + ) + + self._reply_listener.start(timeout=timeout) + + if self._puller_thread is None: + self._puller_thread = threading.Thread(target=self._poller) + self._puller_thread.daemon = True + + if not self._puller_thread.is_alive(): + self._puller_thread.start() + + self._reply_consumer_enabled = True + + return self._reply_queue + + def _poller(self): + while self._reply_consumer_thread_run_flag: + try: + message = self._reply_listener.poll(timeout=1) + if message is None: + continue + i = 0 + curtime = time.time() + while (i < len(self._reply_waiting_future_list) and + self._reply_consumer_thread_run_flag): + msg_id, future, expiration = ( + self._reply_waiting_future_list[i] + ) + if expiration and expiration < curtime: + del self._reply_waiting_future_list[i] + elif msg_id == message.msg_id: + del self._reply_waiting_future_list[i] + future.set_result(message) + else: + i += 1 + except BaseException: + LOG.exception("Exception during reply polling") + + def register_reply_waiter(self, msg_id, future, expiration_time): + self._reply_waiting_future_list.append( + (msg_id, future, expiration_time) + ) + + def cleanup(self): + with self._reply_consumer_lock: + self._reply_consumer_enabled = False + + if self._puller_thread: + if self._puller_thread.is_alive(): + self._reply_consumer_thread_run_flag = False + self._puller_thread.join() + self._puller_thread = None + + if self._reply_listener: + self._reply_listener.stop() + self._reply_listener.cleanup() + self._reply_listener = None + + self._reply_queue = None + + +class PikaIncomingMessage(object): + + def __init__(self, pika_engine, channel, method, properties, body, no_ack): + self._pika_engine = pika_engine + self._no_ack = no_ack + self._channel = channel + self.delivery_tag = method.delivery_tag + + self.content_type = getattr(properties, "content_type", + "application/json") + self.content_encoding = getattr(properties, "content_encoding", + "utf-8") + + self.expiration = ( + None if properties.expiration is None else + int(properties.expiration) + ) + + if self.content_type != "application/json": + raise NotImplementedError("Content-type['{}'] is not valid, " + "'application/json' only is supported.") + + message_dict = common.deserialize_msg( + jsonutils.loads(body, encoding=self.content_encoding) + ) + + self.unique_id = message_dict.pop('_unique_id') + self.msg_id = message_dict.pop('_msg_id', None) + self.reply_q = message_dict.pop('_reply_q', None) + + context_dict = {} + + for key in list(message_dict.keys()): + key = six.text_type(key) + if key.startswith('_context_'): + value = message_dict.pop(key) + context_dict[key[9:]] = value + + self.message = message_dict + self.ctxt = context_dict + + def reply(self, reply=None, failure=None, log_failure=True): + if not (self.msg_id and self.reply_q): + return + + if failure: + failure = common.serialize_remote_exception(failure, log_failure) + + msg = { + 'result': reply, + 'failure': failure, + '_unique_id': uuid.uuid4().hex, + '_msg_id': self.msg_id, + 'ending': True + } + + def on_exception(ex): + if isinstance(ex, ConnectionException): + LOG.warn(str(ex)) + return True + else: + return False + + retrier = retrying.retry( + stop_max_attempt_number=( + None if self._pika_engine.rpc_reply_retry_attempts == -1 + else self._pika_engine.rpc_reply_retry_attempts + ), + retry_on_exception=on_exception, + wait_fixed=self._pika_engine.rpc_reply_retry_delay, + ) + + try: + self._pika_engine.publish( + exchange=self._pika_engine.rpc_reply_exchange, + routing_key=self.reply_q, + body=jsonutils.dumps( + common.serialize_msg(msg), + encoding=self.content_encoding + ), + properties=pika_spec.BasicProperties( + content_encoding=self.content_encoding, + content_type=self.content_type, + ), + confirm=True, + mandatory=False, + expiration_time=time.time() + self.expiration, + retrier=retrier + ) + LOG.debug( + "Message [id:'{}'] replied to '{}'.".format( + self.msg_id, self.reply_q + ) + ) + except Exception: + LOG.exception( + "Message [id:'{}'] wasn't replied to : {}".format( + self.msg_id, self.reply_q + ) + ) + + def acknowledge(self): + if not self._no_ack: + try: + self._channel.basic_ack(delivery_tag=self.delivery_tag) + except Exception: + LOG.exception("Unable to acknowledge the message") + + def requeue(self): + if not self._no_ack: + try: + return self._channel.basic_nack(delivery_tag=self.delivery_tag, + requeue=True) + except Exception: + LOG.exception("Unable to requeue the message") + + +class PikaOutgoingMessage(object): + + def __init__(self, pika_engine, message, context, + content_type="application/json", content_encoding="utf-8"): + self._pika_engine = pika_engine + + self.content_type = content_type + self.content_encoding = content_encoding + + if self.content_type != "application/json": + raise NotImplementedError("Content-type['{}'] is not valid, " + "'application/json' only is supported.") + + self.message = message + self.context = context + + self.unique_id = uuid.uuid4().hex + self.msg_id = None + + def send(self, exchange, routing_key='', confirm=True, + wait_for_reply=False, mandatory=True, persistent=False, + timeout=None, retrier=None): + msg = self.message.copy() + + msg['_unique_id'] = self.unique_id + + for key, value in self.context.iteritems(): + key = six.text_type(key) + msg['_context_' + key] = value + + properties = pika_spec.BasicProperties( + content_encoding=self.content_encoding, + content_type=self.content_type, + delivery_mode=2 if persistent else 1 + ) + + expiration_time = ( + None if timeout is None else timeout + time.time() + ) + + if wait_for_reply: + self.msg_id = uuid.uuid4().hex + msg['_msg_id'] = self.msg_id + LOG.debug('MSG_ID is %s', self.msg_id) + + msg['_reply_q'] = self._pika_engine.get_reply_q(timeout) + + future = futures.Future() + + self._pika_engine.register_reply_waiter( + msg_id=self.msg_id, future=future, + expiration_time=expiration_time + ) + + self._pika_engine.publish( + exchange=exchange, routing_key=routing_key, + body=jsonutils.dumps( + common.serialize_msg(msg), + encoding=self.content_encoding + ), + properties=properties, + confirm=confirm, + mandatory=mandatory, + expiration_time=expiration_time, + retrier=retrier + ) + + if wait_for_reply: + try: + return future.result(timeout) + except futures.TimeoutError: + raise exceptions.MessagingTimeout() + + +class PikaListener(object): + def __init__(self, pika_engine, no_ack, prefetch_count): + self._pika_engine = pika_engine + + self._connection = None + self._channel = None + self._lock = threading.Lock() + + self._prefetch_count = prefetch_count + self._no_ack = no_ack + + self._started = False + + self._message_queue = collections.deque() + + def _reconnect(self): + self._connection = self._pika_engine.create_connection() + self._channel = self._connection.channel() + self._channel.basic_qos(prefetch_count=self._prefetch_count) + + self._on_reconnected() + + def _on_reconnected(self): + raise NotImplementedError( + "It is base class. Please declare consumers here" + ) + + def _start_consuming(self, queue): + self._channel.basic_consume(self._on_message_callback, + queue, no_ack=self._no_ack) + + def _on_message_callback(self, unused, method, properties, body): + self._message_queue.append((self._channel, method, properties, body)) + + def _cleanup(self): + if self._channel: + try: + self._channel.close() + except Exception as ex: + if not pika_pool.Connection.is_connection_invalidated(ex): + LOG.exception("Unexpected error during closing channel") + self._channel = None + + if self._connection: + try: + self._connection.close() + except Exception as ex: + if not pika_pool.Connection.is_connection_invalidated(ex): + LOG.exception("Unexpected error during closing connection") + self._connection = None + + def poll(self, timeout=None): + start = time.time() + while not self._message_queue: + with self._lock: + if not self._started: + return None + if self._channel is None: + self._reconnect() + try: + self._connection.process_data_events() + except pika_pool.Connection.connectivity_errors: + self._cleanup() + if timeout and time.time() - start > timeout: + return None + + return self._message_queue.popleft() + + def start(self): + self._started = True + + def stop(self): + with self._lock: + if not self._started: + return + + self._started = False + self._cleanup() + + def reconnect(self): + with self._lock: + self._cleanup() + try: + self._reconnect() + except Exception: + self._cleanup() + raise + + def cleanup(self): + with self._lock: + self._cleanup() + + +class RpcServicePikaListener(PikaListener): + def __init__(self, pika_engine, target, no_ack=True, prefetch_count=1): + self._target = target + + super(RpcServicePikaListener, self).__init__( + pika_engine, no_ack=no_ack, prefetch_count=prefetch_count) + + def _on_reconnected(self): + exchange = (self._target.exchange or + self._pika_engine.default_rpc_exchange) + queue = '{}'.format(self._target.topic) + server_queue = '{}.{}'.format(queue, self._target.server) + + fanout_exchange = '{}_fanout'.format(self._target.topic) + + queue_expiration = ( + self._pika_engine.conf.oslo_messaging_pika.rpc_queue_expiration + ) + + self._pika_engine.declare_queue_binding( + exchange=exchange, queue=queue, routing_key=queue, + exchange_type='direct', queue_expiration=queue_expiration, + queue_auto_delete=False, durable=False + ) + self._pika_engine.declare_queue_binding( + exchange=exchange, queue=server_queue, routing_key=server_queue, + exchange_type='direct', queue_expiration=queue_expiration, + queue_auto_delete=False, durable=False + ) + self._pika_engine.declare_queue_binding( + exchange=fanout_exchange, queue=server_queue, routing_key="", + exchange_type='fanout', queue_expiration=queue_expiration, + queue_auto_delete=False, durable=False + ) + + self._start_consuming(queue) + self._start_consuming(server_queue) + + def poll(self, timeout=None): + msg = super(RpcServicePikaListener, self).poll(timeout) + if msg is None: + return None + return PikaIncomingMessage( + self._pika_engine, *msg, no_ack=self._no_ack + ) + + +class RpcReplyPikaListener(PikaListener): + def __init__(self, pika_engine, exchange, queue, no_ack=True, + prefetch_count=1): + self._exchange = exchange + self._queue = queue + + super(RpcReplyPikaListener, self).__init__( + pika_engine, no_ack, prefetch_count + ) + + def _on_reconnected(self): + queue_expiration = ( + self._pika_engine.conf.oslo_messaging_pika.rpc_queue_expiration + ) + + self._pika_engine.declare_queue_binding( + exchange=self._exchange, queue=self._queue, + routing_key=self._queue, exchange_type='direct', + queue_expiration=queue_expiration, queue_auto_delete=False, + durable=False + ) + self._start_consuming(self._queue) + + def start(self, timeout=None): + super(RpcReplyPikaListener, self).start() + + def on_exception(ex): + LOG.warn(str(ex)) + + return True + + retrier = retrying.retry( + stop_max_attempt_number=self._pika_engine.rpc_reply_retry_attempts, + stop_max_delay=timeout, + wait_fixed=self._pika_engine.rpc_reply_retry_delay, + retry_on_exception=on_exception, + ) + + retrier(self.reconnect)() + + def poll(self, timeout=None): + msg = super(RpcReplyPikaListener, self).poll(timeout) + if msg is None: + return None + return PikaIncomingMessage( + self._pika_engine, *msg, no_ack=self._no_ack + ) + + +class NotificationPikaListener(PikaListener): + def __init__(self, pika_engine, targets_and_priorities, + queue_name=None, prefetch_count=100): + self._targets_and_priorities = targets_and_priorities + self._queue_name = queue_name + + super(NotificationPikaListener, self).__init__( + pika_engine, no_ack=False, prefetch_count=prefetch_count + ) + + def _on_reconnected(self): + queues_to_consume = set() + for target, priority in self._targets_and_priorities: + routing_key = '%s.%s' % (target.topic, priority) + queue = self._queue_name or routing_key + self._pika_engine.declare_queue_binding( + exchange=( + target.exchange or + self._pika_engine.default_notification_exchange + ), + queue = queue, + routing_key=routing_key, + exchange_type='direct', + queue_expiration=None, + queue_auto_delete=False, + durable=self._pika_engine.notification_persistence, + ) + queues_to_consume.add(queue) + + for queue_to_consume in queues_to_consume: + self._start_consuming(queue_to_consume) + + def poll(self, timeout=None): + msg = super(NotificationPikaListener, self).poll(timeout) + if msg is None: + return None + return PikaIncomingMessage( + self._pika_engine, *msg, no_ack=self._no_ack + ) + + +class PikaDriver(object): + def __init__(self, conf, url, default_exchange=None, + allowed_remote_exmods=None): + if 'eventlet.patcher' in sys.modules: + import eventlet.patcher + if eventlet.patcher.is_monkey_patched('select'): + import select + + try: + del select.poll + except AttributeError: + pass + + try: + del select.epoll + except AttributeError: + pass + + opt_group = cfg.OptGroup(name='oslo_messaging_pika', + title='Pika driver options') + conf.register_group(opt_group) + conf.register_opts(pika_opts, group=opt_group) + conf.register_opts(pika_pool_opts, group=opt_group) + conf.register_opts(rpc_opts, group=opt_group) + conf.register_opts(notification_opts, group=opt_group) + + self.conf = conf + self._allowed_remote_exmods = allowed_remote_exmods + + self._pika_engine = PikaEngine(conf, url, default_exchange) + + def require_features(self, requeue=False): + pass + + def send(self, target, ctxt, message, wait_for_reply=None, timeout=None, + retry=None): + + if retry is None: + retry = self._pika_engine.default_rpc_retry_attempts + + def on_exception(ex): + if isinstance(ex, (ConnectionException, + exceptions.MessageDeliveryFailure)): + LOG.warn(str(ex)) + return True + else: + return False + + retrier = ( + None if retry == 0 else + retrying.retry( + stop_max_attempt_number=(None if retry == -1 else retry), + retry_on_exception=on_exception, + wait_fixed=self._pika_engine.rpc_retry_delay, + ) + ) + + msg = PikaOutgoingMessage(self._pika_engine, message, ctxt) + + if target.fanout: + return msg.send( + exchange='{}_fanout'.format(target.topic), + timeout=timeout, confirm=True, mandatory=False, + retrier=retrier + ) + + queue = target.topic + if target.server: + queue = '{}.{}'.format(queue, target.server) + + reply = msg.send( + exchange=target.exchange or self._pika_engine.default_rpc_exchange, + routing_key=queue, + wait_for_reply=wait_for_reply, + timeout=timeout, + confirm=True, + mandatory=True, + retrier=retrier + ) + + if reply is not None: + if reply.message['failure']: + ex = common.deserialize_remote_exception( + reply.message['failure'], self._allowed_remote_exmods + ) + raise ex + + return reply.message['result'] + + def send_notification(self, target, ctxt, message, version, retry=None): + if retry is None: + retry = self._pika_engine.default_notification_retry_attempts + + def on_exception(ex): + if isinstance(ex, (ExchangeNotFoundException, RoutingException)): + LOG.warn(str(ex)) + try: + self._pika_engine.declare_queue_binding( + exchange=( + target.exchange or + self._pika_engine.default_notification_exchange + ), + queue=target.topic, + routing_key=target.topic, + exchange_type='direct', + queue_expiration=False, + queue_auto_delete=False, + durable=self._pika_engine.notification_persistence, + ) + except ConnectionException as e: + LOG.warn(str(e)) + return True + elif isinstance(ex, + (ConnectionException, MessageRejectedException)): + return True + else: + return False + + retrier = retrying.retry( + stop_max_attempt_number=(None if retry == -1 else retry), + retry_on_exception=on_exception, + wait_fixed=self._pika_engine.notification_retry_delay, + ) + + msg = PikaOutgoingMessage(self._pika_engine, message, ctxt) + + return msg.send( + exchange=( + target.exchange or + self._pika_engine.default_notification_exchange + ), + routing_key=target.topic, + wait_for_reply=False, + confirm=True, + mandatory=True, + persistent=self._pika_engine.notification_persistence, + retrier=retrier + ) + + def listen(self, target): + listener = RpcServicePikaListener(self._pika_engine, target) + listener.start() + return listener + + def listen_for_notifications(self, targets_and_priorities, pool): + listener = NotificationPikaListener(self._pika_engine, + targets_and_priorities, pool) + listener.start() + return listener + + def cleanup(self): + self._pika_engine.cleanup() + + +class PikaDriverCompatibleWithRabbitDriver(PikaDriver): + """Old RabbitMQ driver creates exchange before sending message. + In this case if no rpc service listen this exchange message will be sent + to /dev/null but client will know anything about it. That is strange. + But for now we need to keep original behaviour + """ + def send(self, target, ctxt, message, wait_for_reply=None, timeout=None, + retry=None): + try: + return super(PikaDriverCompatibleWithRabbitDriver, self).send( + target=target, + ctxt=ctxt, + message=message, + wait_for_reply=wait_for_reply, + timeout=timeout, + retry=retry + ) + except exceptions.MessageDeliveryFailure: + if wait_for_reply: + raise exceptions.MessagingTimeout() + else: + return None diff --git a/requirements.txt b/requirements.txt index f00a9dc37..681d55ec9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -33,6 +33,8 @@ PyYAML>=3.1.0 # we set the amqp version to ensure heartbeat works amqp>=1.4.0 kombu>=3.0.7 +pika>=0.10.0 +pika-pool>=0.1.2 # middleware oslo.middleware>=2.8.0 # Apache-2.0 diff --git a/setup-test-env-pika.sh b/setup-test-env-pika.sh new file mode 100755 index 000000000..5fe189555 --- /dev/null +++ b/setup-test-env-pika.sh @@ -0,0 +1,32 @@ +#!/bin/bash +set -e + +. tools/functions.sh + +DATADIR=$(mktemp -d /tmp/OSLOMSG-RABBIT.XXXXX) +trap "clean_exit $DATADIR" EXIT + +export RABBITMQ_NODE_IP_ADDRESS=127.0.0.1 +export RABBITMQ_NODE_PORT=65123 +export RABBITMQ_NODENAME=oslomsg-test@localhost +export RABBITMQ_LOG_BASE=$DATADIR +export RABBITMQ_MNESIA_BASE=$DATADIR +export RABBITMQ_PID_FILE=$DATADIR/pid +export HOME=$DATADIR + +# NOTE(sileht): We directly use the rabbitmq scripts +# to avoid distribution check, like running as root/rabbitmq +# enforcing. +export PATH=/usr/lib/rabbitmq/bin/:$PATH + + +mkfifo ${DATADIR}/out +rabbitmq-server &> ${DATADIR}/out & +wait_for_line "Starting broker... completed" "ERROR:" ${DATADIR}/out + +rabbitmqctl add_user oslomsg oslosecret +rabbitmqctl set_permissions "oslomsg" ".*" ".*" ".*" + + +export TRANSPORT_URL=pika://oslomsg:oslosecret@127.0.0.1:65123// +$* diff --git a/setup.cfg b/setup.cfg index ee63dc5bb..1524e0467 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,6 +35,7 @@ oslo.messaging.drivers = # This is just for internal testing fake = oslo_messaging._drivers.impl_fake:FakeDriver + pika = oslo_messaging._drivers.impl_pika:PikaDriverCompatibleWithRabbitDriver oslo.messaging.executors = aioeventlet = oslo_messaging._executors.impl_aioeventlet:AsyncioEventletExecutor diff --git a/tox.ini b/tox.ini index c576bed72..bb4a69a6a 100644 --- a/tox.ini +++ b/tox.ini @@ -30,6 +30,9 @@ commands = {toxinidir}/setup-test-env-qpid.sh 0-10 python setup.py testr --slowe [testenv:py27-func-rabbit] commands = {toxinidir}/setup-test-env-rabbit.sh python setup.py testr --slowest --testr-args='oslo_messaging.tests.functional' +[testenv:py27-func-pika] +commands = {toxinidir}/setup-test-env-pika.sh python setup.py testr --slowest --testr-args='oslo_messaging.tests.functional' + [testenv:py27-func-amqp1] setenv = TRANSPORT_URL=amqp://stackqpid:secretqpid@127.0.0.1:65123// # NOTE(flaper87): This gate job run on fedora21 for now. From 9cae182b4939b5d7e267598327122bea89540384 Mon Sep 17 00:00:00 2001 From: dukhlov Date: Sat, 24 Oct 2015 17:13:06 -0400 Subject: [PATCH 03/16] Fix fanout exchange name pattern Change-Id: I0e3a85fe9bbd4a597b555302aa2c1c24045d2eec --- oslo_messaging/_drivers/impl_pika.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/oslo_messaging/_drivers/impl_pika.py b/oslo_messaging/_drivers/impl_pika.py index 84aa6882b..8dfe3bda8 100644 --- a/oslo_messaging/_drivers/impl_pika.py +++ b/oslo_messaging/_drivers/impl_pika.py @@ -812,7 +812,9 @@ class RpcServicePikaListener(PikaListener): queue = '{}'.format(self._target.topic) server_queue = '{}.{}'.format(queue, self._target.server) - fanout_exchange = '{}_fanout'.format(self._target.topic) + fanout_exchange = '{}_fanout_{}'.format( + self._pika_engine.default_rpc_exchange, self._target.topic + ) queue_expiration = ( self._pika_engine.conf.oslo_messaging_pika.rpc_queue_expiration @@ -997,7 +999,9 @@ class PikaDriver(object): if target.fanout: return msg.send( - exchange='{}_fanout'.format(target.topic), + exchange='{}_fanout_{}'.format( + self._pika_engine.default_rpc_exchange, target.topic + ), timeout=timeout, confirm=True, mandatory=False, retrier=retrier ) From 968d3e6741ac95a0988ee5cbd2fee2ad53697d58 Mon Sep 17 00:00:00 2001 From: Dmitriy Ukhlov Date: Mon, 2 Nov 2015 16:05:59 +0200 Subject: [PATCH 04/16] Fixes and improvements after testing on RabbitMQ cluster: 1) adds tcp_user_timeout parameter - timiout for unacked tcp pockets 2) adds host_connection_reconnect_delay parameter - delay for reconnection to some host if error occurs during connection. It allows to use other hosts if we have some host disconnected 3) adds rpc_listener_ack and rpc_listener_prefetch_count properties - enable consumer acknowledges and set maximum number of unacknowledged messages 4) fixes time units (in oslo.messaging it is seconds but in RabbitMQ - milliseconds) Change-Id: Ifd549a1eebeef27a3d36ceb6d3e8b1c76ea00b65 --- oslo_messaging/_drivers/impl_pika.py | 344 +++++++++++++++++++-------- 1 file changed, 250 insertions(+), 94 deletions(-) diff --git a/oslo_messaging/_drivers/impl_pika.py b/oslo_messaging/_drivers/impl_pika.py index 8dfe3bda8..fc6de2c82 100644 --- a/oslo_messaging/_drivers/impl_pika.py +++ b/oslo_messaging/_drivers/impl_pika.py @@ -24,6 +24,7 @@ import pika_pool import retrying import six +import socket import sys import threading import time @@ -46,30 +47,36 @@ pika_opts = [ help='Maximum number of channels to allow'), cfg.IntOpt('frame_max', default=None, help='The maximum byte size for an AMQP frame'), - cfg.IntOpt('heartbeat_interval', default=None, - help='How often to send heartbeats'), + cfg.IntOpt('heartbeat_interval', default=1, + help="How often to send heartbeats for consumer's connections"), cfg.BoolOpt('ssl', default=None, help='Enable SSL'), cfg.DictOpt('ssl_options', default=None, help='Arguments passed to ssl.wrap_socket'), - cfg.FloatOpt('socket_timeout', default=None, - help='Use for high latency networks'), + cfg.FloatOpt('socket_timeout', default=0.25, + help="Set socket timeout in seconds for connection's socket"), + cfg.FloatOpt('tcp_user_timeout', default=0.25, + help="Set TCP_USER_TIMEOUT in seconds for connection's " + "socket"), + cfg.FloatOpt('host_connection_reconnect_delay', default=5, + help="Set delay for reconnection to some host which has " + "connection error") ] pika_pool_opts = [ cfg.IntOpt('pool_max_size', default=10, help="Maximum number of connections to keep queued."), - cfg.IntOpt('pool_max_overflow', default=10, + cfg.IntOpt('pool_max_overflow', default=0, help="Maximum number of connections to create above " "`pool_max_size`."), cfg.IntOpt('pool_timeout', default=30, help="Default number of seconds to wait for a connections to " "available"), - cfg.IntOpt('pool_recycle', default=None, + cfg.IntOpt('pool_recycle', default=600, help="Lifetime of a connection (since creation) in seconds " "or None for no recycling. Expired connections are " "closed on acquire."), - cfg.IntOpt('pool_stale', default=None, + cfg.IntOpt('pool_stale', default=60, help="Threshold at which inactive (since release) connections " "are considered stale in seconds or None for no " "staleness. Stale connections are closed on acquire.") @@ -87,7 +94,7 @@ notification_opts = [ "sending notification, -1 means infinite retry." ), cfg.FloatOpt( - 'notification_retry_delay', default=0.1, + 'notification_retry_delay', default=0.25, help="Reconnecting retry delay in case of connectivity problem during " "sending notification message" ) @@ -101,23 +108,45 @@ rpc_opts = [ help="Exchange name for for sending RPC messages"), cfg.StrOpt('rpc_reply_exchange', default="${control_exchange}_rpc_reply", help="Exchange name for for receiving RPC replies"), + + cfg.BoolOpt('rpc_listener_ack', default=True, + help="Disable to increase performance. If disabled - some " + "messages may be lost in case of connectivity problem. " + "If enabled - may cause not needed message redelivery " + "and rpc request could be processed more then one time"), + cfg.BoolOpt('rpc_reply_listener_ack', default=True, + help="Disable to increase performance. If disabled - some " + "replies may be lost in case of connectivity problem."), cfg.IntOpt( - 'rpc_reply_retry_attempts', default=3, + 'rpc_listener_prefetch_count', default=10, + help="Max number of not acknowledged message which RabbitMQ can send " + "to rpc listener. Works only if rpc_listener_ack == True" + ), + cfg.IntOpt( + 'rpc_reply_listener_prefetch_count', default=10, + help="Max number of not acknowledged message which RabbitMQ can send " + "to rpc reply listener. Works only if rpc_reply_listener_ack == " + "True" + ), + cfg.IntOpt( + 'rpc_reply_retry_attempts', default=-1, help="Reconnecting retry count in case of connectivity problem during " - "sending reply. -1 means infinite retry." + "sending reply. -1 means infinite retry during rpc_timeout" ), cfg.FloatOpt( - 'rpc_reply_retry_delay', default=0.1, + 'rpc_reply_retry_delay', default=0.25, help="Reconnecting retry delay in case of connectivity problem during " "sending reply." ), cfg.IntOpt( - 'default_rpc_retry_attempts', default=0, + 'default_rpc_retry_attempts', default=-1, help="Reconnecting retry count in case of connectivity problem during " - "sending RPC message, -1 means infinite retry." + "sending RPC message, -1 means infinite retry. If actual " + "retry attempts in not 0 the rpc request could be processed more " + "then one time" ), cfg.FloatOpt( - 'rpc_retry_delay', default=0.1, + 'rpc_retry_delay', default=0.25, help="Reconnecting retry delay in case of connectivity problem during " "sending RPC message" ) @@ -147,10 +176,18 @@ class ConnectionException(exceptions.MessagingException): pass +class HostConnectionNotAllowedException(ConnectionException): + pass + + class EstablishConnectionException(ConnectionException): pass +class TimeoutConnectionException(ConnectionException): + pass + + class PooledConnectionWithConfirmations(pika_pool.Connection): @property def channel(self): @@ -161,9 +198,16 @@ class PooledConnectionWithConfirmations(pika_pool.Connection): class PikaEngine(object): + HOST_CONNECTION_LAST_TRY_TIME = "last_try_time" + HOST_CONNECTION_LAST_SUCCESS_TRY_TIME = "last_success_try_time" + + TCP_USER_TIMEOUT = 18 + def __init__(self, conf, url, default_exchange=None): self.conf = conf + # processing rpc options + self.default_rpc_exchange = ( conf.oslo_messaging_pika.default_rpc_exchange if conf.oslo_messaging_pika.default_rpc_exchange else @@ -175,14 +219,18 @@ class PikaEngine(object): default_exchange ) - self.default_notification_exchange = ( - conf.oslo_messaging_pika.default_notification_exchange if - conf.oslo_messaging_pika.default_notification_exchange else - default_exchange + self.rpc_listener_ack = conf.oslo_messaging_pika.rpc_listener_ack + + self.rpc_reply_listener_ack = ( + conf.oslo_messaging_pika.rpc_reply_listener_ack ) - self.notification_persistence = ( - conf.oslo_messaging_pika.notification_persistence + self.rpc_listener_prefetch_count = ( + conf.oslo_messaging_pika.rpc_listener_prefetch_count + ) + + self.rpc_reply_listener_prefetch_count = ( + conf.oslo_messaging_pika.rpc_listener_prefetch_count ) self.rpc_reply_retry_attempts = ( @@ -198,6 +246,17 @@ class PikaEngine(object): raise ValueError("rpc_reply_retry_delay should be non-negative " "integer") + # processing notification options + self.default_notification_exchange = ( + conf.oslo_messaging_pika.default_notification_exchange if + conf.oslo_messaging_pika.default_notification_exchange else + default_exchange + ) + + self.notification_persistence = ( + conf.oslo_messaging_pika.notification_persistence + ) + self.default_rpc_retry_attempts = ( conf.oslo_messaging_pika.default_rpc_retry_attempts ) @@ -235,32 +294,41 @@ class PikaEngine(object): self._reply_consumer_lock = threading.Lock() self._puller_thread = None + self._tcp_user_timeout = self.conf.oslo_messaging_pika.tcp_user_timeout + self._host_connection_reconnect_delay = ( + self.conf.oslo_messaging_pika.host_connection_reconnect_delay + ) + # initializing connection parameters for configured RabbitMQ hosts - self._pika_next_connection_num = 0 common_pika_params = { 'virtual_host': url.virtual_host, 'channel_max': self.conf.oslo_messaging_pika.channel_max, 'frame_max': self.conf.oslo_messaging_pika.frame_max, - 'heartbeat_interval': - self.conf.oslo_messaging_pika.heartbeat_interval, 'ssl': self.conf.oslo_messaging_pika.ssl, 'ssl_options': self.conf.oslo_messaging_pika.ssl_options, 'socket_timeout': self.conf.oslo_messaging_pika.socket_timeout, } - self._pika_params_list = [] - self._create_connection_lock = threading.Lock() + self._connection_lock = threading.Lock() + + self._connection_host_param_list = [] + self._connection_host_status_list = [] + self._next_connection_host_num = 0 for transport_host in url.hosts: - pika_params = pika.ConnectionParameters( + pika_params = common_pika_params.copy() + pika_params.update( host=transport_host.hostname, port=transport_host.port, credentials=pika_credentials.PlainCredentials( transport_host.username, transport_host.password ), - **common_pika_params ) - self._pika_params_list.append(pika_params) + self._connection_host_param_list.append(pika_params) + self._connection_host_status_list.append({ + self.HOST_CONNECTION_LAST_TRY_TIME: 0, + self.HOST_CONNECTION_LAST_SUCCESS_TRY_TIME: 0 + }) # initializing 2 connection pools: 1st for connections without # confirmations, 2nd - with confirmations @@ -286,40 +354,124 @@ class PikaEngine(object): PooledConnectionWithConfirmations ) - def create_connection(self): + def _next_connection_num(self): + with self._connection_lock: + cur_num = self._next_connection_host_num + self._next_connection_host_num += 1 + self._next_connection_host_num %= len( + self._connection_host_param_list + ) + return cur_num + + def create_connection(self, for_listening=False): """Create and return connection to any available host. :return: cerated connection :raise: ConnectionException if all hosts are not reachable """ - host_num = len(self._pika_params_list) - connection_attempts = host_num + host_count = len(self._connection_host_param_list) + connection_attempts = host_count + + pika_next_connection_num = self._next_connection_num() + while connection_attempts > 0: - with self._create_connection_lock: - try: - return self.create_host_connection( - self._pika_next_connection_num - ) - except pika_pool.Connection.connectivity_errors as e: - LOG.warn(str(e)) - connection_attempts -= 1 - continue - finally: - self._pika_next_connection_num += 1 - self._pika_next_connection_num %= host_num + try: + return self.create_host_connection( + pika_next_connection_num, for_listening + ) + except pika_pool.Connection.connectivity_errors as e: + LOG.warn(str(e)) + except HostConnectionNotAllowedException as e: + LOG.warn(str(e)) + + connection_attempts -= 1 + pika_next_connection_num += 1 + pika_next_connection_num %= host_count + raise EstablishConnectionException( "Can not establish connection to any configured RabbitMQ host: " + - str(self._pika_params_list) + str(self._connection_host_param_list) ) - def create_host_connection(self, host_index): + def _set_tcp_user_timeout(self, s): + if not self._tcp_user_timeout: + return + try: + s.setsockopt( + socket.IPPROTO_TCP, self.TCP_USER_TIMEOUT, + int(self._tcp_user_timeout * 1000) + ) + except socket.error: + LOG.warn( + "Whoops, this kernel doesn't seem to support TCP_USER_TIMEOUT." + ) + + def create_host_connection(self, host_index, for_listening=False): """Create new connection to host #host_index :return: New connection """ - return pika_adapters.BlockingConnection( - self._pika_params_list[host_index] + + with self._connection_lock: + cur_time = time.time() + + last_success_time = self._connection_host_status_list[host_index][ + self.HOST_CONNECTION_LAST_SUCCESS_TRY_TIME + ] + last_time = self._connection_host_status_list[host_index][ + self.HOST_CONNECTION_LAST_TRY_TIME + ] + if (last_time != last_success_time and + cur_time - last_time < + self._host_connection_reconnect_delay): + raise HostConnectionNotAllowedException( + "Connection to host #{} is not allowed now because of " + "previous failure".format(host_index) + ) + + try: + base_host_params = self._connection_host_param_list[host_index] + + connection = pika_adapters.BlockingConnection( + pika.ConnectionParameters( + heartbeat_interval=( + self.conf.oslo_messaging_pika.heartbeat_interval + if for_listening else None + ), + **base_host_params + ) + ) + + self._set_tcp_user_timeout(connection._impl.socket) + + self._connection_host_status_list[host_index][ + self.HOST_CONNECTION_LAST_SUCCESS_TRY_TIME + ] = cur_time + + return connection + finally: + self._connection_host_status_list[host_index][ + self.HOST_CONNECTION_LAST_TRY_TIME + ] = cur_time + + @staticmethod + def declare_queue_binding_by_channel(channel, exchange, queue, routing_key, + exchange_type, queue_expiration, + queue_auto_delete, durable): + channel.exchange_declare( + exchange, exchange_type, auto_delete=True, durable=durable ) + arguments = {} + + if queue_expiration > 0: + arguments['x-expires'] = queue_expiration * 1000 + + channel.queue_declare( + queue, auto_delete=queue_auto_delete, durable=durable, + arguments=arguments + ) + + channel.queue_bind(queue, exchange, routing_key) def declare_queue_binding(self, exchange, queue, routing_key, exchange_type, queue_expiration, @@ -331,20 +483,10 @@ class PikaEngine(object): ) try: with self.connection_pool.acquire(timeout=timeout) as conn: - conn.channel.exchange_declare( - exchange, exchange_type, auto_delete=True, durable=durable + self.declare_queue_binding_by_channel( + conn.channel, exchange, queue, routing_key, exchange_type, + queue_expiration, queue_auto_delete, durable ) - arguments = {} - - if queue_expiration > 0: - arguments['x-expires'] = queue_expiration * 1000 - - conn.channel.queue_declare( - queue, auto_delete=queue_auto_delete, durable=durable, - arguments=arguments - ) - - conn.channel.queue_bind(queue, exchange, routing_key) except pika_pool.Timeout as e: raise exceptions.MessagingTimeout( "Timeout for current operation was expired. {}.".format(str(e)) @@ -369,11 +511,10 @@ class PikaEngine(object): raise exceptions.MessagingTimeout( "Timeout for current operation was expired." ) - try: with pool.acquire(timeout=timeout) as conn: if timeout is not None: - properties.expiration = str(int(timeout)) + properties.expiration = str(int(timeout * 1000)) conn.channel.publish( exchange=exchange, routing_key=routing_key, @@ -410,6 +551,7 @@ class PikaEngine(object): body, properties, exchange, routing_key, str(e) ) ) + raise ConnectionException( "Connectivity problem detected during sending the message: " "[body:{}, properties: {}] to target: [exchange:{}, " @@ -417,6 +559,10 @@ class PikaEngine(object): body, properties, exchange, routing_key, str(e) ) ) + except socket.timeout: + raise TimeoutConnectionException( + "Socket timeout exceeded." + ) def publish(self, exchange, routing_key, body, properties, confirm, mandatory, expiration_time, retrier): @@ -454,6 +600,8 @@ class PikaEngine(object): pika_engine=self, exchange=self.rpc_reply_exchange, queue=self._reply_queue, + no_ack=not self.rpc_reply_listener_ack, + prefetch_count=self.rpc_reply_listener_prefetch_count ) self._reply_listener.start(timeout=timeout) @@ -473,6 +621,7 @@ class PikaEngine(object): while self._reply_consumer_thread_run_flag: try: message = self._reply_listener.poll(timeout=1) + message.acknowledge() if message is None: continue i = 0 @@ -528,9 +677,9 @@ class PikaIncomingMessage(object): self.content_encoding = getattr(properties, "content_encoding", "utf-8") - self.expiration = ( + self.expiration_time = ( None if properties.expiration is None else - int(properties.expiration) + time.time() + int(properties.expiration) / 1000 ) if self.content_type != "application/json": @@ -584,7 +733,7 @@ class PikaIncomingMessage(object): else self._pika_engine.rpc_reply_retry_attempts ), retry_on_exception=on_exception, - wait_fixed=self._pika_engine.rpc_reply_retry_delay, + wait_fixed=self._pika_engine.rpc_reply_retry_delay * 1000, ) try: @@ -601,7 +750,7 @@ class PikaIncomingMessage(object): ), confirm=True, mandatory=False, - expiration_time=time.time() + self.expiration, + expiration_time=self.expiration_time, retrier=retrier ) LOG.debug( @@ -618,18 +767,12 @@ class PikaIncomingMessage(object): def acknowledge(self): if not self._no_ack: - try: - self._channel.basic_ack(delivery_tag=self.delivery_tag) - except Exception: - LOG.exception("Unable to acknowledge the message") + self._channel.basic_ack(delivery_tag=self.delivery_tag) def requeue(self): if not self._no_ack: - try: - return self._channel.basic_nack(delivery_tag=self.delivery_tag, - requeue=True) - except Exception: - LOG.exception("Unable to requeue the message") + return self._channel.basic_nack(delivery_tag=self.delivery_tag, + requeue=True) class PikaOutgoingMessage(object): @@ -669,7 +812,7 @@ class PikaOutgoingMessage(object): ) expiration_time = ( - None if timeout is None else timeout + time.time() + None if timeout is None else (timeout + time.time()) ) if wait_for_reply: @@ -722,7 +865,9 @@ class PikaListener(object): self._message_queue = collections.deque() def _reconnect(self): - self._connection = self._pika_engine.create_connection() + self._connection = self._pika_engine.create_connection( + for_listening=True + ) self._channel = self._connection.channel() self._channel.basic_qos(prefetch_count=self._prefetch_count) @@ -763,12 +908,14 @@ class PikaListener(object): with self._lock: if not self._started: return None - if self._channel is None: - self._reconnect() + try: + if self._channel is None: + self._reconnect() self._connection.process_data_events() - except pika_pool.Connection.connectivity_errors: + except Exception: self._cleanup() + raise if timeout and time.time() - start > timeout: return None @@ -800,7 +947,7 @@ class PikaListener(object): class RpcServicePikaListener(PikaListener): - def __init__(self, pika_engine, target, no_ack=True, prefetch_count=1): + def __init__(self, pika_engine, target, no_ack, prefetch_count): self._target = target super(RpcServicePikaListener, self).__init__( @@ -820,17 +967,20 @@ class RpcServicePikaListener(PikaListener): self._pika_engine.conf.oslo_messaging_pika.rpc_queue_expiration ) - self._pika_engine.declare_queue_binding( + self._pika_engine.declare_queue_binding_by_channel( + channel=self._channel, exchange=exchange, queue=queue, routing_key=queue, exchange_type='direct', queue_expiration=queue_expiration, queue_auto_delete=False, durable=False ) - self._pika_engine.declare_queue_binding( + self._pika_engine.declare_queue_binding_by_channel( + channel=self._channel, exchange=exchange, queue=server_queue, routing_key=server_queue, exchange_type='direct', queue_expiration=queue_expiration, queue_auto_delete=False, durable=False ) - self._pika_engine.declare_queue_binding( + self._pika_engine.declare_queue_binding_by_channel( + channel=self._channel, exchange=fanout_exchange, queue=server_queue, routing_key="", exchange_type='fanout', queue_expiration=queue_expiration, queue_auto_delete=False, durable=False @@ -849,8 +999,7 @@ class RpcServicePikaListener(PikaListener): class RpcReplyPikaListener(PikaListener): - def __init__(self, pika_engine, exchange, queue, no_ack=True, - prefetch_count=1): + def __init__(self, pika_engine, exchange, queue, no_ack, prefetch_count): self._exchange = exchange self._queue = queue @@ -863,7 +1012,8 @@ class RpcReplyPikaListener(PikaListener): self._pika_engine.conf.oslo_messaging_pika.rpc_queue_expiration ) - self._pika_engine.declare_queue_binding( + self._pika_engine.declare_queue_binding_by_channel( + channel=self._channel, exchange=self._exchange, queue=self._queue, routing_key=self._queue, exchange_type='direct', queue_expiration=queue_expiration, queue_auto_delete=False, @@ -881,8 +1031,8 @@ class RpcReplyPikaListener(PikaListener): retrier = retrying.retry( stop_max_attempt_number=self._pika_engine.rpc_reply_retry_attempts, - stop_max_delay=timeout, - wait_fixed=self._pika_engine.rpc_reply_retry_delay, + stop_max_delay=timeout * 1000, + wait_fixed=self._pika_engine.rpc_reply_retry_delay * 1000, retry_on_exception=on_exception, ) @@ -912,7 +1062,8 @@ class NotificationPikaListener(PikaListener): for target, priority in self._targets_and_priorities: routing_key = '%s.%s' % (target.topic, priority) queue = self._queue_name or routing_key - self._pika_engine.declare_queue_binding( + self._pika_engine.declare_queue_binding_by_channel( + channel=self._channel, exchange=( target.exchange or self._pika_engine.default_notification_exchange @@ -991,7 +1142,7 @@ class PikaDriver(object): retrying.retry( stop_max_attempt_number=(None if retry == -1 else retry), retry_on_exception=on_exception, - wait_fixed=self._pika_engine.rpc_retry_delay, + wait_fixed=self._pika_engine.rpc_retry_delay * 1000, ) ) @@ -1045,7 +1196,7 @@ class PikaDriver(object): queue=target.topic, routing_key=target.topic, exchange_type='direct', - queue_expiration=False, + queue_expiration=None, queue_auto_delete=False, durable=self._pika_engine.notification_persistence, ) @@ -1054,6 +1205,7 @@ class PikaDriver(object): return True elif isinstance(ex, (ConnectionException, MessageRejectedException)): + LOG.warn(str(ex)) return True else: return False @@ -1061,7 +1213,7 @@ class PikaDriver(object): retrier = retrying.retry( stop_max_attempt_number=(None if retry == -1 else retry), retry_on_exception=on_exception, - wait_fixed=self._pika_engine.notification_retry_delay, + wait_fixed=self._pika_engine.notification_retry_delay * 1000, ) msg = PikaOutgoingMessage(self._pika_engine, message, ctxt) @@ -1080,7 +1232,11 @@ class PikaDriver(object): ) def listen(self, target): - listener = RpcServicePikaListener(self._pika_engine, target) + listener = RpcServicePikaListener( + self._pika_engine, target, + no_ack=not self._pika_engine.rpc_listener_ack, + prefetch_count=self._pika_engine.rpc_listener_prefetch_count + ) listener.start() return listener From 8737dea0bee731ad74f4a00261932f98e00364bb Mon Sep 17 00:00:00 2001 From: Dmitriy Ukhlov Date: Tue, 3 Nov 2015 17:26:56 +0200 Subject: [PATCH 05/16] Splits pika driver into several files Change-Id: Ieff35e94b2a62137bc72820a8b1c83561dea5765 --- oslo_messaging/_drivers/impl_pika.py | 978 +----------------- .../_drivers/pika_driver/__init__.py | 0 .../_drivers/pika_driver/pika_engine.py | 510 +++++++++ .../_drivers/pika_driver/pika_exceptions.py | 43 + .../_drivers/pika_driver/pika_listener.py | 266 +++++ .../_drivers/pika_driver/pika_message.py | 217 ++++ 6 files changed, 1056 insertions(+), 958 deletions(-) create mode 100644 oslo_messaging/_drivers/pika_driver/__init__.py create mode 100644 oslo_messaging/_drivers/pika_driver/pika_engine.py create mode 100644 oslo_messaging/_drivers/pika_driver/pika_exceptions.py create mode 100644 oslo_messaging/_drivers/pika_driver/pika_listener.py create mode 100644 oslo_messaging/_drivers/pika_driver/pika_message.py diff --git a/oslo_messaging/_drivers/impl_pika.py b/oslo_messaging/_drivers/impl_pika.py index fc6de2c82..085901a16 100644 --- a/oslo_messaging/_drivers/impl_pika.py +++ b/oslo_messaging/_drivers/impl_pika.py @@ -11,35 +11,22 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -import collections -from concurrent import futures -import pika -from pika import adapters as pika_adapters -from pika import credentials as pika_credentials -from pika import exceptions as pika_exceptions -from pika import spec as pika_spec - -import pika_pool import retrying -import six -import socket import sys -import threading -import time -import uuid - from oslo_config import cfg from oslo_log import log as logging from oslo_messaging._drivers import common +from oslo_messaging._drivers.pika_driver import pika_engine +from oslo_messaging._drivers.pika_driver import pika_exceptions as pika_drv_exc +from oslo_messaging._drivers.pika_driver import pika_listener +from oslo_messaging._drivers.pika_driver import pika_message + from oslo_messaging import exceptions -from oslo_serialization import jsonutils - - LOG = logging.getLogger(__name__) pika_opts = [ @@ -160,935 +147,6 @@ def _is_eventlet_monkey_patched(): return eventlet.patcher.is_monkey_patched('thread') -class ExchangeNotFoundException(exceptions.MessageDeliveryFailure): - pass - - -class MessageRejectedException(exceptions.MessageDeliveryFailure): - pass - - -class RoutingException(exceptions.MessageDeliveryFailure): - pass - - -class ConnectionException(exceptions.MessagingException): - pass - - -class HostConnectionNotAllowedException(ConnectionException): - pass - - -class EstablishConnectionException(ConnectionException): - pass - - -class TimeoutConnectionException(ConnectionException): - pass - - -class PooledConnectionWithConfirmations(pika_pool.Connection): - @property - def channel(self): - if self.fairy.channel is None: - self.fairy.channel = self.fairy.cxn.channel() - self.fairy.channel.confirm_delivery() - return self.fairy.channel - - -class PikaEngine(object): - HOST_CONNECTION_LAST_TRY_TIME = "last_try_time" - HOST_CONNECTION_LAST_SUCCESS_TRY_TIME = "last_success_try_time" - - TCP_USER_TIMEOUT = 18 - - def __init__(self, conf, url, default_exchange=None): - self.conf = conf - - # processing rpc options - - self.default_rpc_exchange = ( - conf.oslo_messaging_pika.default_rpc_exchange if - conf.oslo_messaging_pika.default_rpc_exchange else - default_exchange - ) - self.rpc_reply_exchange = ( - conf.oslo_messaging_pika.rpc_reply_exchange if - conf.oslo_messaging_pika.rpc_reply_exchange else - default_exchange - ) - - self.rpc_listener_ack = conf.oslo_messaging_pika.rpc_listener_ack - - self.rpc_reply_listener_ack = ( - conf.oslo_messaging_pika.rpc_reply_listener_ack - ) - - self.rpc_listener_prefetch_count = ( - conf.oslo_messaging_pika.rpc_listener_prefetch_count - ) - - self.rpc_reply_listener_prefetch_count = ( - conf.oslo_messaging_pika.rpc_listener_prefetch_count - ) - - self.rpc_reply_retry_attempts = ( - conf.oslo_messaging_pika.rpc_reply_retry_attempts - ) - if self.rpc_reply_retry_attempts is None: - raise ValueError("rpc_reply_retry_attempts should be integer") - self.rpc_reply_retry_delay = ( - conf.oslo_messaging_pika.rpc_reply_retry_delay - ) - if (self.rpc_reply_retry_delay is None or - self.rpc_reply_retry_delay < 0): - raise ValueError("rpc_reply_retry_delay should be non-negative " - "integer") - - # processing notification options - self.default_notification_exchange = ( - conf.oslo_messaging_pika.default_notification_exchange if - conf.oslo_messaging_pika.default_notification_exchange else - default_exchange - ) - - self.notification_persistence = ( - conf.oslo_messaging_pika.notification_persistence - ) - - self.default_rpc_retry_attempts = ( - conf.oslo_messaging_pika.default_rpc_retry_attempts - ) - if self.default_rpc_retry_attempts is None: - raise ValueError("default_rpc_retry_attempts should be an integer") - self.rpc_retry_delay = ( - conf.oslo_messaging_pika.rpc_retry_delay - ) - if (self.rpc_retry_delay is None or - self.rpc_retry_delay < 0): - raise ValueError("rpc_retry_delay should be non-negative integer") - - self.default_notification_retry_attempts = ( - conf.oslo_messaging_pika.default_notification_retry_attempts - ) - if self.default_notification_retry_attempts is None: - raise ValueError("default_notification_retry_attempts should be " - "an integer") - self.notification_retry_delay = ( - conf.oslo_messaging_pika.notification_retry_delay - ) - if (self.notification_retry_delay is None or - self.notification_retry_delay < 0): - raise ValueError("notification_retry_delay should be non-negative " - "integer") - - # preparing poller for listening replies - self._reply_queue = None - - self._reply_listener = None - self._reply_waiting_future_list = [] - - self._reply_consumer_enabled = False - self._reply_consumer_thread_run_flag = True - self._reply_consumer_lock = threading.Lock() - self._puller_thread = None - - self._tcp_user_timeout = self.conf.oslo_messaging_pika.tcp_user_timeout - self._host_connection_reconnect_delay = ( - self.conf.oslo_messaging_pika.host_connection_reconnect_delay - ) - - # initializing connection parameters for configured RabbitMQ hosts - common_pika_params = { - 'virtual_host': url.virtual_host, - 'channel_max': self.conf.oslo_messaging_pika.channel_max, - 'frame_max': self.conf.oslo_messaging_pika.frame_max, - 'ssl': self.conf.oslo_messaging_pika.ssl, - 'ssl_options': self.conf.oslo_messaging_pika.ssl_options, - 'socket_timeout': self.conf.oslo_messaging_pika.socket_timeout, - } - - self._connection_lock = threading.Lock() - - self._connection_host_param_list = [] - self._connection_host_status_list = [] - self._next_connection_host_num = 0 - - for transport_host in url.hosts: - pika_params = common_pika_params.copy() - pika_params.update( - host=transport_host.hostname, - port=transport_host.port, - credentials=pika_credentials.PlainCredentials( - transport_host.username, transport_host.password - ), - ) - self._connection_host_param_list.append(pika_params) - self._connection_host_status_list.append({ - self.HOST_CONNECTION_LAST_TRY_TIME: 0, - self.HOST_CONNECTION_LAST_SUCCESS_TRY_TIME: 0 - }) - - # initializing 2 connection pools: 1st for connections without - # confirmations, 2nd - with confirmations - self.connection_pool = pika_pool.QueuedPool( - create=self.create_connection, - max_size=self.conf.oslo_messaging_pika.pool_max_size, - max_overflow=self.conf.oslo_messaging_pika.pool_max_overflow, - timeout=self.conf.oslo_messaging_pika.pool_timeout, - recycle=self.conf.oslo_messaging_pika.pool_recycle, - stale=self.conf.oslo_messaging_pika.pool_stale, - ) - - self.connection_with_confirmation_pool = pika_pool.QueuedPool( - create=self.create_connection, - max_size=self.conf.oslo_messaging_pika.pool_max_size, - max_overflow=self.conf.oslo_messaging_pika.pool_max_overflow, - timeout=self.conf.oslo_messaging_pika.pool_timeout, - recycle=self.conf.oslo_messaging_pika.pool_recycle, - stale=self.conf.oslo_messaging_pika.pool_stale, - ) - - self.connection_with_confirmation_pool.Connection = ( - PooledConnectionWithConfirmations - ) - - def _next_connection_num(self): - with self._connection_lock: - cur_num = self._next_connection_host_num - self._next_connection_host_num += 1 - self._next_connection_host_num %= len( - self._connection_host_param_list - ) - return cur_num - - def create_connection(self, for_listening=False): - """Create and return connection to any available host. - - :return: cerated connection - :raise: ConnectionException if all hosts are not reachable - """ - host_count = len(self._connection_host_param_list) - connection_attempts = host_count - - pika_next_connection_num = self._next_connection_num() - - while connection_attempts > 0: - try: - return self.create_host_connection( - pika_next_connection_num, for_listening - ) - except pika_pool.Connection.connectivity_errors as e: - LOG.warn(str(e)) - except HostConnectionNotAllowedException as e: - LOG.warn(str(e)) - - connection_attempts -= 1 - pika_next_connection_num += 1 - pika_next_connection_num %= host_count - - raise EstablishConnectionException( - "Can not establish connection to any configured RabbitMQ host: " + - str(self._connection_host_param_list) - ) - - def _set_tcp_user_timeout(self, s): - if not self._tcp_user_timeout: - return - try: - s.setsockopt( - socket.IPPROTO_TCP, self.TCP_USER_TIMEOUT, - int(self._tcp_user_timeout * 1000) - ) - except socket.error: - LOG.warn( - "Whoops, this kernel doesn't seem to support TCP_USER_TIMEOUT." - ) - - def create_host_connection(self, host_index, for_listening=False): - """Create new connection to host #host_index - - :return: New connection - """ - - with self._connection_lock: - cur_time = time.time() - - last_success_time = self._connection_host_status_list[host_index][ - self.HOST_CONNECTION_LAST_SUCCESS_TRY_TIME - ] - last_time = self._connection_host_status_list[host_index][ - self.HOST_CONNECTION_LAST_TRY_TIME - ] - if (last_time != last_success_time and - cur_time - last_time < - self._host_connection_reconnect_delay): - raise HostConnectionNotAllowedException( - "Connection to host #{} is not allowed now because of " - "previous failure".format(host_index) - ) - - try: - base_host_params = self._connection_host_param_list[host_index] - - connection = pika_adapters.BlockingConnection( - pika.ConnectionParameters( - heartbeat_interval=( - self.conf.oslo_messaging_pika.heartbeat_interval - if for_listening else None - ), - **base_host_params - ) - ) - - self._set_tcp_user_timeout(connection._impl.socket) - - self._connection_host_status_list[host_index][ - self.HOST_CONNECTION_LAST_SUCCESS_TRY_TIME - ] = cur_time - - return connection - finally: - self._connection_host_status_list[host_index][ - self.HOST_CONNECTION_LAST_TRY_TIME - ] = cur_time - - @staticmethod - def declare_queue_binding_by_channel(channel, exchange, queue, routing_key, - exchange_type, queue_expiration, - queue_auto_delete, durable): - channel.exchange_declare( - exchange, exchange_type, auto_delete=True, durable=durable - ) - arguments = {} - - if queue_expiration > 0: - arguments['x-expires'] = queue_expiration * 1000 - - channel.queue_declare( - queue, auto_delete=queue_auto_delete, durable=durable, - arguments=arguments - ) - - channel.queue_bind(queue, exchange, routing_key) - - def declare_queue_binding(self, exchange, queue, routing_key, - exchange_type, queue_expiration, - queue_auto_delete, durable, - timeout=None): - if timeout is not None and timeout < 0: - raise exceptions.MessagingTimeout( - "Timeout for current operation was expired." - ) - try: - with self.connection_pool.acquire(timeout=timeout) as conn: - self.declare_queue_binding_by_channel( - conn.channel, exchange, queue, routing_key, exchange_type, - queue_expiration, queue_auto_delete, durable - ) - except pika_pool.Timeout as e: - raise exceptions.MessagingTimeout( - "Timeout for current operation was expired. {}.".format(str(e)) - ) - except pika_pool.Connection.connectivity_errors as e: - raise ConnectionException( - "Connectivity problem detected during declaring queue " - "binding: exchange:{}, queue: {}, routing_key: {}, " - "exchange_type: {}, queue_expiration: {}, queue_auto_delete: " - "{}, durable: {}. {}".format( - exchange, queue, routing_key, exchange_type, - queue_expiration, queue_auto_delete, durable, str(e) - ) - ) - - @staticmethod - def _do_publish(pool, exchange, routing_key, body, properties, - mandatory, expiration_time): - timeout = (None if expiration_time is None else - expiration_time - time.time()) - if timeout is not None and timeout < 0: - raise exceptions.MessagingTimeout( - "Timeout for current operation was expired." - ) - try: - with pool.acquire(timeout=timeout) as conn: - if timeout is not None: - properties.expiration = str(int(timeout * 1000)) - conn.channel.publish( - exchange=exchange, - routing_key=routing_key, - body=body, - properties=properties, - mandatory=mandatory - ) - except pika_exceptions.NackError as e: - raise MessageRejectedException( - "Can not send message: [body: {}], properties: {}] to " - "target [exchange: {}, routing_key: {}]. {}".format( - body, properties, exchange, routing_key, str(e) - ) - ) - except pika_exceptions.UnroutableError as e: - raise RoutingException( - "Can not deliver message:[body:{}, properties: {}] to any" - "queue using target: [exchange:{}, " - "routing_key:{}]. {}".format( - body, properties, exchange, routing_key, str(e) - ) - ) - except pika_pool.Timeout as e: - raise exceptions.MessagingTimeout( - "Timeout for current operation was expired. {}".format(str(e)) - ) - except pika_pool.Connection.connectivity_errors as e: - if (isinstance(e, pika_exceptions.ChannelClosed) - and e.args and e.args[0] == 404): - raise ExchangeNotFoundException( - "Attempt to send message to not existing exchange " - "detected, message: [body:{}, properties: {}], target: " - "[exchange:{}, routing_key:{}]. {}".format( - body, properties, exchange, routing_key, str(e) - ) - ) - - raise ConnectionException( - "Connectivity problem detected during sending the message: " - "[body:{}, properties: {}] to target: [exchange:{}, " - "routing_key:{}]. {}".format( - body, properties, exchange, routing_key, str(e) - ) - ) - except socket.timeout: - raise TimeoutConnectionException( - "Socket timeout exceeded." - ) - - def publish(self, exchange, routing_key, body, properties, confirm, - mandatory, expiration_time, retrier): - pool = (self.connection_with_confirmation_pool if confirm else - self.connection_pool) - - LOG.debug( - "Sending message:[body:{}; properties: {}] to target: " - "[exchange:{}; routing_key:{}]".format( - body, properties, exchange, routing_key - ) - ) - - do_publish = (self._do_publish if retrier is None else - retrier(self._do_publish)) - - return do_publish(pool, exchange, routing_key, body, properties, - mandatory, expiration_time) - - def get_reply_q(self, timeout=None): - if self._reply_consumer_enabled: - return self._reply_queue - - with self._reply_consumer_lock: - if self._reply_consumer_enabled: - return self._reply_queue - - if self._reply_queue is None: - self._reply_queue = "reply.{}.{}.{}".format( - self.conf.project, self.conf.prog, uuid.uuid4().hex - ) - - if self._reply_listener is None: - self._reply_listener = RpcReplyPikaListener( - pika_engine=self, - exchange=self.rpc_reply_exchange, - queue=self._reply_queue, - no_ack=not self.rpc_reply_listener_ack, - prefetch_count=self.rpc_reply_listener_prefetch_count - ) - - self._reply_listener.start(timeout=timeout) - - if self._puller_thread is None: - self._puller_thread = threading.Thread(target=self._poller) - self._puller_thread.daemon = True - - if not self._puller_thread.is_alive(): - self._puller_thread.start() - - self._reply_consumer_enabled = True - - return self._reply_queue - - def _poller(self): - while self._reply_consumer_thread_run_flag: - try: - message = self._reply_listener.poll(timeout=1) - message.acknowledge() - if message is None: - continue - i = 0 - curtime = time.time() - while (i < len(self._reply_waiting_future_list) and - self._reply_consumer_thread_run_flag): - msg_id, future, expiration = ( - self._reply_waiting_future_list[i] - ) - if expiration and expiration < curtime: - del self._reply_waiting_future_list[i] - elif msg_id == message.msg_id: - del self._reply_waiting_future_list[i] - future.set_result(message) - else: - i += 1 - except BaseException: - LOG.exception("Exception during reply polling") - - def register_reply_waiter(self, msg_id, future, expiration_time): - self._reply_waiting_future_list.append( - (msg_id, future, expiration_time) - ) - - def cleanup(self): - with self._reply_consumer_lock: - self._reply_consumer_enabled = False - - if self._puller_thread: - if self._puller_thread.is_alive(): - self._reply_consumer_thread_run_flag = False - self._puller_thread.join() - self._puller_thread = None - - if self._reply_listener: - self._reply_listener.stop() - self._reply_listener.cleanup() - self._reply_listener = None - - self._reply_queue = None - - -class PikaIncomingMessage(object): - - def __init__(self, pika_engine, channel, method, properties, body, no_ack): - self._pika_engine = pika_engine - self._no_ack = no_ack - self._channel = channel - self.delivery_tag = method.delivery_tag - - self.content_type = getattr(properties, "content_type", - "application/json") - self.content_encoding = getattr(properties, "content_encoding", - "utf-8") - - self.expiration_time = ( - None if properties.expiration is None else - time.time() + int(properties.expiration) / 1000 - ) - - if self.content_type != "application/json": - raise NotImplementedError("Content-type['{}'] is not valid, " - "'application/json' only is supported.") - - message_dict = common.deserialize_msg( - jsonutils.loads(body, encoding=self.content_encoding) - ) - - self.unique_id = message_dict.pop('_unique_id') - self.msg_id = message_dict.pop('_msg_id', None) - self.reply_q = message_dict.pop('_reply_q', None) - - context_dict = {} - - for key in list(message_dict.keys()): - key = six.text_type(key) - if key.startswith('_context_'): - value = message_dict.pop(key) - context_dict[key[9:]] = value - - self.message = message_dict - self.ctxt = context_dict - - def reply(self, reply=None, failure=None, log_failure=True): - if not (self.msg_id and self.reply_q): - return - - if failure: - failure = common.serialize_remote_exception(failure, log_failure) - - msg = { - 'result': reply, - 'failure': failure, - '_unique_id': uuid.uuid4().hex, - '_msg_id': self.msg_id, - 'ending': True - } - - def on_exception(ex): - if isinstance(ex, ConnectionException): - LOG.warn(str(ex)) - return True - else: - return False - - retrier = retrying.retry( - stop_max_attempt_number=( - None if self._pika_engine.rpc_reply_retry_attempts == -1 - else self._pika_engine.rpc_reply_retry_attempts - ), - retry_on_exception=on_exception, - wait_fixed=self._pika_engine.rpc_reply_retry_delay * 1000, - ) - - try: - self._pika_engine.publish( - exchange=self._pika_engine.rpc_reply_exchange, - routing_key=self.reply_q, - body=jsonutils.dumps( - common.serialize_msg(msg), - encoding=self.content_encoding - ), - properties=pika_spec.BasicProperties( - content_encoding=self.content_encoding, - content_type=self.content_type, - ), - confirm=True, - mandatory=False, - expiration_time=self.expiration_time, - retrier=retrier - ) - LOG.debug( - "Message [id:'{}'] replied to '{}'.".format( - self.msg_id, self.reply_q - ) - ) - except Exception: - LOG.exception( - "Message [id:'{}'] wasn't replied to : {}".format( - self.msg_id, self.reply_q - ) - ) - - def acknowledge(self): - if not self._no_ack: - self._channel.basic_ack(delivery_tag=self.delivery_tag) - - def requeue(self): - if not self._no_ack: - return self._channel.basic_nack(delivery_tag=self.delivery_tag, - requeue=True) - - -class PikaOutgoingMessage(object): - - def __init__(self, pika_engine, message, context, - content_type="application/json", content_encoding="utf-8"): - self._pika_engine = pika_engine - - self.content_type = content_type - self.content_encoding = content_encoding - - if self.content_type != "application/json": - raise NotImplementedError("Content-type['{}'] is not valid, " - "'application/json' only is supported.") - - self.message = message - self.context = context - - self.unique_id = uuid.uuid4().hex - self.msg_id = None - - def send(self, exchange, routing_key='', confirm=True, - wait_for_reply=False, mandatory=True, persistent=False, - timeout=None, retrier=None): - msg = self.message.copy() - - msg['_unique_id'] = self.unique_id - - for key, value in self.context.iteritems(): - key = six.text_type(key) - msg['_context_' + key] = value - - properties = pika_spec.BasicProperties( - content_encoding=self.content_encoding, - content_type=self.content_type, - delivery_mode=2 if persistent else 1 - ) - - expiration_time = ( - None if timeout is None else (timeout + time.time()) - ) - - if wait_for_reply: - self.msg_id = uuid.uuid4().hex - msg['_msg_id'] = self.msg_id - LOG.debug('MSG_ID is %s', self.msg_id) - - msg['_reply_q'] = self._pika_engine.get_reply_q(timeout) - - future = futures.Future() - - self._pika_engine.register_reply_waiter( - msg_id=self.msg_id, future=future, - expiration_time=expiration_time - ) - - self._pika_engine.publish( - exchange=exchange, routing_key=routing_key, - body=jsonutils.dumps( - common.serialize_msg(msg), - encoding=self.content_encoding - ), - properties=properties, - confirm=confirm, - mandatory=mandatory, - expiration_time=expiration_time, - retrier=retrier - ) - - if wait_for_reply: - try: - return future.result(timeout) - except futures.TimeoutError: - raise exceptions.MessagingTimeout() - - -class PikaListener(object): - def __init__(self, pika_engine, no_ack, prefetch_count): - self._pika_engine = pika_engine - - self._connection = None - self._channel = None - self._lock = threading.Lock() - - self._prefetch_count = prefetch_count - self._no_ack = no_ack - - self._started = False - - self._message_queue = collections.deque() - - def _reconnect(self): - self._connection = self._pika_engine.create_connection( - for_listening=True - ) - self._channel = self._connection.channel() - self._channel.basic_qos(prefetch_count=self._prefetch_count) - - self._on_reconnected() - - def _on_reconnected(self): - raise NotImplementedError( - "It is base class. Please declare consumers here" - ) - - def _start_consuming(self, queue): - self._channel.basic_consume(self._on_message_callback, - queue, no_ack=self._no_ack) - - def _on_message_callback(self, unused, method, properties, body): - self._message_queue.append((self._channel, method, properties, body)) - - def _cleanup(self): - if self._channel: - try: - self._channel.close() - except Exception as ex: - if not pika_pool.Connection.is_connection_invalidated(ex): - LOG.exception("Unexpected error during closing channel") - self._channel = None - - if self._connection: - try: - self._connection.close() - except Exception as ex: - if not pika_pool.Connection.is_connection_invalidated(ex): - LOG.exception("Unexpected error during closing connection") - self._connection = None - - def poll(self, timeout=None): - start = time.time() - while not self._message_queue: - with self._lock: - if not self._started: - return None - - try: - if self._channel is None: - self._reconnect() - self._connection.process_data_events() - except Exception: - self._cleanup() - raise - if timeout and time.time() - start > timeout: - return None - - return self._message_queue.popleft() - - def start(self): - self._started = True - - def stop(self): - with self._lock: - if not self._started: - return - - self._started = False - self._cleanup() - - def reconnect(self): - with self._lock: - self._cleanup() - try: - self._reconnect() - except Exception: - self._cleanup() - raise - - def cleanup(self): - with self._lock: - self._cleanup() - - -class RpcServicePikaListener(PikaListener): - def __init__(self, pika_engine, target, no_ack, prefetch_count): - self._target = target - - super(RpcServicePikaListener, self).__init__( - pika_engine, no_ack=no_ack, prefetch_count=prefetch_count) - - def _on_reconnected(self): - exchange = (self._target.exchange or - self._pika_engine.default_rpc_exchange) - queue = '{}'.format(self._target.topic) - server_queue = '{}.{}'.format(queue, self._target.server) - - fanout_exchange = '{}_fanout_{}'.format( - self._pika_engine.default_rpc_exchange, self._target.topic - ) - - queue_expiration = ( - self._pika_engine.conf.oslo_messaging_pika.rpc_queue_expiration - ) - - self._pika_engine.declare_queue_binding_by_channel( - channel=self._channel, - exchange=exchange, queue=queue, routing_key=queue, - exchange_type='direct', queue_expiration=queue_expiration, - queue_auto_delete=False, durable=False - ) - self._pika_engine.declare_queue_binding_by_channel( - channel=self._channel, - exchange=exchange, queue=server_queue, routing_key=server_queue, - exchange_type='direct', queue_expiration=queue_expiration, - queue_auto_delete=False, durable=False - ) - self._pika_engine.declare_queue_binding_by_channel( - channel=self._channel, - exchange=fanout_exchange, queue=server_queue, routing_key="", - exchange_type='fanout', queue_expiration=queue_expiration, - queue_auto_delete=False, durable=False - ) - - self._start_consuming(queue) - self._start_consuming(server_queue) - - def poll(self, timeout=None): - msg = super(RpcServicePikaListener, self).poll(timeout) - if msg is None: - return None - return PikaIncomingMessage( - self._pika_engine, *msg, no_ack=self._no_ack - ) - - -class RpcReplyPikaListener(PikaListener): - def __init__(self, pika_engine, exchange, queue, no_ack, prefetch_count): - self._exchange = exchange - self._queue = queue - - super(RpcReplyPikaListener, self).__init__( - pika_engine, no_ack, prefetch_count - ) - - def _on_reconnected(self): - queue_expiration = ( - self._pika_engine.conf.oslo_messaging_pika.rpc_queue_expiration - ) - - self._pika_engine.declare_queue_binding_by_channel( - channel=self._channel, - exchange=self._exchange, queue=self._queue, - routing_key=self._queue, exchange_type='direct', - queue_expiration=queue_expiration, queue_auto_delete=False, - durable=False - ) - self._start_consuming(self._queue) - - def start(self, timeout=None): - super(RpcReplyPikaListener, self).start() - - def on_exception(ex): - LOG.warn(str(ex)) - - return True - - retrier = retrying.retry( - stop_max_attempt_number=self._pika_engine.rpc_reply_retry_attempts, - stop_max_delay=timeout * 1000, - wait_fixed=self._pika_engine.rpc_reply_retry_delay * 1000, - retry_on_exception=on_exception, - ) - - retrier(self.reconnect)() - - def poll(self, timeout=None): - msg = super(RpcReplyPikaListener, self).poll(timeout) - if msg is None: - return None - return PikaIncomingMessage( - self._pika_engine, *msg, no_ack=self._no_ack - ) - - -class NotificationPikaListener(PikaListener): - def __init__(self, pika_engine, targets_and_priorities, - queue_name=None, prefetch_count=100): - self._targets_and_priorities = targets_and_priorities - self._queue_name = queue_name - - super(NotificationPikaListener, self).__init__( - pika_engine, no_ack=False, prefetch_count=prefetch_count - ) - - def _on_reconnected(self): - queues_to_consume = set() - for target, priority in self._targets_and_priorities: - routing_key = '%s.%s' % (target.topic, priority) - queue = self._queue_name or routing_key - self._pika_engine.declare_queue_binding_by_channel( - channel=self._channel, - exchange=( - target.exchange or - self._pika_engine.default_notification_exchange - ), - queue = queue, - routing_key=routing_key, - exchange_type='direct', - queue_expiration=None, - queue_auto_delete=False, - durable=self._pika_engine.notification_persistence, - ) - queues_to_consume.add(queue) - - for queue_to_consume in queues_to_consume: - self._start_consuming(queue_to_consume) - - def poll(self, timeout=None): - msg = super(NotificationPikaListener, self).poll(timeout) - if msg is None: - return None - return PikaIncomingMessage( - self._pika_engine, *msg, no_ack=self._no_ack - ) - - class PikaDriver(object): def __init__(self, conf, url, default_exchange=None, allowed_remote_exmods=None): @@ -1118,7 +176,7 @@ class PikaDriver(object): self.conf = conf self._allowed_remote_exmods = allowed_remote_exmods - self._pika_engine = PikaEngine(conf, url, default_exchange) + self._pika_engine = pika_engine.PikaEngine(conf, url, default_exchange) def require_features(self, requeue=False): pass @@ -1130,7 +188,7 @@ class PikaDriver(object): retry = self._pika_engine.default_rpc_retry_attempts def on_exception(ex): - if isinstance(ex, (ConnectionException, + if isinstance(ex, (pika_drv_exc.ConnectionException, exceptions.MessageDeliveryFailure)): LOG.warn(str(ex)) return True @@ -1146,7 +204,8 @@ class PikaDriver(object): ) ) - msg = PikaOutgoingMessage(self._pika_engine, message, ctxt) + msg = pika_message.PikaOutgoingMessage(self._pika_engine, message, + ctxt) if target.fanout: return msg.send( @@ -1185,7 +244,8 @@ class PikaDriver(object): retry = self._pika_engine.default_notification_retry_attempts def on_exception(ex): - if isinstance(ex, (ExchangeNotFoundException, RoutingException)): + if isinstance(ex, (pika_drv_exc.ExchangeNotFoundException, + pika_drv_exc.RoutingException)): LOG.warn(str(ex)) try: self._pika_engine.declare_queue_binding( @@ -1200,11 +260,11 @@ class PikaDriver(object): queue_auto_delete=False, durable=self._pika_engine.notification_persistence, ) - except ConnectionException as e: + except pika_drv_exc.ConnectionException as e: LOG.warn(str(e)) return True - elif isinstance(ex, - (ConnectionException, MessageRejectedException)): + elif isinstance(ex, (pika_drv_exc.ConnectionException, + pika_drv_exc.MessageRejectedException)): LOG.warn(str(ex)) return True else: @@ -1216,7 +276,8 @@ class PikaDriver(object): wait_fixed=self._pika_engine.notification_retry_delay * 1000, ) - msg = PikaOutgoingMessage(self._pika_engine, message, ctxt) + msg = pika_message.PikaOutgoingMessage(self._pika_engine, message, + ctxt) return msg.send( exchange=( @@ -1232,7 +293,7 @@ class PikaDriver(object): ) def listen(self, target): - listener = RpcServicePikaListener( + listener = pika_listener.RpcServicePikaListener( self._pika_engine, target, no_ack=not self._pika_engine.rpc_listener_ack, prefetch_count=self._pika_engine.rpc_listener_prefetch_count @@ -1241,8 +302,9 @@ class PikaDriver(object): return listener def listen_for_notifications(self, targets_and_priorities, pool): - listener = NotificationPikaListener(self._pika_engine, - targets_and_priorities, pool) + listener = pika_listener.NotificationPikaListener( + self._pika_engine, targets_and_priorities, pool + ) listener.start() return listener diff --git a/oslo_messaging/_drivers/pika_driver/__init__.py b/oslo_messaging/_drivers/pika_driver/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/oslo_messaging/_drivers/pika_driver/pika_engine.py b/oslo_messaging/_drivers/pika_driver/pika_engine.py new file mode 100644 index 000000000..991824d64 --- /dev/null +++ b/oslo_messaging/_drivers/pika_driver/pika_engine.py @@ -0,0 +1,510 @@ +# Copyright 2015 Mirantis, 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. +import socket + +from oslo_log import log as logging + +from oslo_messaging import exceptions + +from oslo_messaging._drivers.pika_driver import pika_exceptions as pika_drv_exc +from oslo_messaging._drivers.pika_driver import pika_listener + +import pika +from pika import adapters as pika_adapters +from pika import credentials as pika_credentials +from pika import exceptions as pika_exceptions + +import pika_pool + +import threading +import time +import uuid + +LOG = logging.getLogger(__name__) + + +class PooledConnectionWithConfirmations(pika_pool.Connection): + @property + def channel(self): + if self.fairy.channel is None: + self.fairy.channel = self.fairy.cxn.channel() + self.fairy.channel.confirm_delivery() + return self.fairy.channel + + +class PikaEngine(object): + HOST_CONNECTION_LAST_TRY_TIME = "last_try_time" + HOST_CONNECTION_LAST_SUCCESS_TRY_TIME = "last_success_try_time" + + TCP_USER_TIMEOUT = 18 + + def __init__(self, conf, url, default_exchange=None): + self.conf = conf + + # processing rpc options + + self.default_rpc_exchange = ( + conf.oslo_messaging_pika.default_rpc_exchange if + conf.oslo_messaging_pika.default_rpc_exchange else + default_exchange + ) + self.rpc_reply_exchange = ( + conf.oslo_messaging_pika.rpc_reply_exchange if + conf.oslo_messaging_pika.rpc_reply_exchange else + default_exchange + ) + + self.rpc_listener_ack = conf.oslo_messaging_pika.rpc_listener_ack + + self.rpc_reply_listener_ack = ( + conf.oslo_messaging_pika.rpc_reply_listener_ack + ) + + self.rpc_listener_prefetch_count = ( + conf.oslo_messaging_pika.rpc_listener_prefetch_count + ) + + self.rpc_reply_listener_prefetch_count = ( + conf.oslo_messaging_pika.rpc_listener_prefetch_count + ) + + self.rpc_reply_retry_attempts = ( + conf.oslo_messaging_pika.rpc_reply_retry_attempts + ) + if self.rpc_reply_retry_attempts is None: + raise ValueError("rpc_reply_retry_attempts should be integer") + self.rpc_reply_retry_delay = ( + conf.oslo_messaging_pika.rpc_reply_retry_delay + ) + if (self.rpc_reply_retry_delay is None or + self.rpc_reply_retry_delay < 0): + raise ValueError("rpc_reply_retry_delay should be non-negative " + "integer") + + # processing notification options + self.default_notification_exchange = ( + conf.oslo_messaging_pika.default_notification_exchange if + conf.oslo_messaging_pika.default_notification_exchange else + default_exchange + ) + + self.notification_persistence = ( + conf.oslo_messaging_pika.notification_persistence + ) + + self.default_rpc_retry_attempts = ( + conf.oslo_messaging_pika.default_rpc_retry_attempts + ) + if self.default_rpc_retry_attempts is None: + raise ValueError("default_rpc_retry_attempts should be an integer") + self.rpc_retry_delay = ( + conf.oslo_messaging_pika.rpc_retry_delay + ) + if (self.rpc_retry_delay is None or + self.rpc_retry_delay < 0): + raise ValueError("rpc_retry_delay should be non-negative integer") + + self.default_notification_retry_attempts = ( + conf.oslo_messaging_pika.default_notification_retry_attempts + ) + if self.default_notification_retry_attempts is None: + raise ValueError("default_notification_retry_attempts should be " + "an integer") + self.notification_retry_delay = ( + conf.oslo_messaging_pika.notification_retry_delay + ) + if (self.notification_retry_delay is None or + self.notification_retry_delay < 0): + raise ValueError("notification_retry_delay should be non-negative " + "integer") + + # preparing poller for listening replies + self._reply_queue = None + + self._reply_listener = None + self._reply_waiting_future_list = [] + + self._reply_consumer_enabled = False + self._reply_consumer_thread_run_flag = True + self._reply_consumer_lock = threading.Lock() + self._puller_thread = None + + self._tcp_user_timeout = self.conf.oslo_messaging_pika.tcp_user_timeout + self._host_connection_reconnect_delay = ( + self.conf.oslo_messaging_pika.host_connection_reconnect_delay + ) + + # initializing connection parameters for configured RabbitMQ hosts + common_pika_params = { + 'virtual_host': url.virtual_host, + 'channel_max': self.conf.oslo_messaging_pika.channel_max, + 'frame_max': self.conf.oslo_messaging_pika.frame_max, + 'ssl': self.conf.oslo_messaging_pika.ssl, + 'ssl_options': self.conf.oslo_messaging_pika.ssl_options, + 'socket_timeout': self.conf.oslo_messaging_pika.socket_timeout, + } + + self._connection_lock = threading.Lock() + + self._connection_host_param_list = [] + self._connection_host_status_list = [] + self._next_connection_host_num = 0 + + for transport_host in url.hosts: + pika_params = common_pika_params.copy() + pika_params.update( + host=transport_host.hostname, + port=transport_host.port, + credentials=pika_credentials.PlainCredentials( + transport_host.username, transport_host.password + ), + ) + self._connection_host_param_list.append(pika_params) + self._connection_host_status_list.append({ + self.HOST_CONNECTION_LAST_TRY_TIME: 0, + self.HOST_CONNECTION_LAST_SUCCESS_TRY_TIME: 0 + }) + + # initializing 2 connection pools: 1st for connections without + # confirmations, 2nd - with confirmations + self.connection_pool = pika_pool.QueuedPool( + create=self.create_connection, + max_size=self.conf.oslo_messaging_pika.pool_max_size, + max_overflow=self.conf.oslo_messaging_pika.pool_max_overflow, + timeout=self.conf.oslo_messaging_pika.pool_timeout, + recycle=self.conf.oslo_messaging_pika.pool_recycle, + stale=self.conf.oslo_messaging_pika.pool_stale, + ) + + self.connection_with_confirmation_pool = pika_pool.QueuedPool( + create=self.create_connection, + max_size=self.conf.oslo_messaging_pika.pool_max_size, + max_overflow=self.conf.oslo_messaging_pika.pool_max_overflow, + timeout=self.conf.oslo_messaging_pika.pool_timeout, + recycle=self.conf.oslo_messaging_pika.pool_recycle, + stale=self.conf.oslo_messaging_pika.pool_stale, + ) + + self.connection_with_confirmation_pool.Connection = ( + PooledConnectionWithConfirmations + ) + + def _next_connection_num(self): + with self._connection_lock: + cur_num = self._next_connection_host_num + self._next_connection_host_num += 1 + self._next_connection_host_num %= len( + self._connection_host_param_list + ) + return cur_num + + def create_connection(self, for_listening=False): + """Create and return connection to any available host. + + :return: cerated connection + :raise: ConnectionException if all hosts are not reachable + """ + host_count = len(self._connection_host_param_list) + connection_attempts = host_count + + pika_next_connection_num = self._next_connection_num() + + while connection_attempts > 0: + try: + return self.create_host_connection( + pika_next_connection_num, for_listening + ) + except pika_pool.Connection.connectivity_errors as e: + LOG.warn(str(e)) + except pika_drv_exc.HostConnectionNotAllowedException as e: + LOG.warn(str(e)) + + connection_attempts -= 1 + pika_next_connection_num += 1 + pika_next_connection_num %= host_count + + raise pika_drv_exc.EstablishConnectionException( + "Can not establish connection to any configured RabbitMQ host: " + + str(self._connection_host_param_list) + ) + + def _set_tcp_user_timeout(self, s): + if not self._tcp_user_timeout: + return + try: + s.setsockopt( + socket.IPPROTO_TCP, self.TCP_USER_TIMEOUT, + int(self._tcp_user_timeout * 1000) + ) + except socket.error: + LOG.warn( + "Whoops, this kernel doesn't seem to support TCP_USER_TIMEOUT." + ) + + def create_host_connection(self, host_index, for_listening=False): + """Create new connection to host #host_index + + :return: New connection + """ + + with self._connection_lock: + cur_time = time.time() + + last_success_time = self._connection_host_status_list[host_index][ + self.HOST_CONNECTION_LAST_SUCCESS_TRY_TIME + ] + last_time = self._connection_host_status_list[host_index][ + self.HOST_CONNECTION_LAST_TRY_TIME + ] + if (last_time != last_success_time and + cur_time - last_time < + self._host_connection_reconnect_delay): + raise pika_drv_exc.HostConnectionNotAllowedException( + "Connection to host #{} is not allowed now because of " + "previous failure".format(host_index) + ) + + try: + base_host_params = self._connection_host_param_list[host_index] + + connection = pika_adapters.BlockingConnection( + pika.ConnectionParameters( + heartbeat_interval=( + self.conf.oslo_messaging_pika.heartbeat_interval + if for_listening else None + ), + **base_host_params + ) + ) + + self._set_tcp_user_timeout(connection._impl.socket) + + self._connection_host_status_list[host_index][ + self.HOST_CONNECTION_LAST_SUCCESS_TRY_TIME + ] = cur_time + + return connection + finally: + self._connection_host_status_list[host_index][ + self.HOST_CONNECTION_LAST_TRY_TIME + ] = cur_time + + @staticmethod + def declare_queue_binding_by_channel(channel, exchange, queue, routing_key, + exchange_type, queue_expiration, + queue_auto_delete, durable): + channel.exchange_declare( + exchange, exchange_type, auto_delete=True, durable=durable + ) + arguments = {} + + if queue_expiration > 0: + arguments['x-expires'] = queue_expiration * 1000 + + channel.queue_declare( + queue, auto_delete=queue_auto_delete, durable=durable, + arguments=arguments + ) + + channel.queue_bind(queue, exchange, routing_key) + + def declare_queue_binding(self, exchange, queue, routing_key, + exchange_type, queue_expiration, + queue_auto_delete, durable, + timeout=None): + if timeout is not None and timeout < 0: + raise exceptions.MessagingTimeout( + "Timeout for current operation was expired." + ) + try: + with self.connection_pool.acquire(timeout=timeout) as conn: + self.declare_queue_binding_by_channel( + conn.channel, exchange, queue, routing_key, exchange_type, + queue_expiration, queue_auto_delete, durable + ) + except pika_pool.Timeout as e: + raise exceptions.MessagingTimeout( + "Timeout for current operation was expired. {}.".format(str(e)) + ) + except pika_pool.Connection.connectivity_errors as e: + raise pika_drv_exc.ConnectionException( + "Connectivity problem detected during declaring queue " + "binding: exchange:{}, queue: {}, routing_key: {}, " + "exchange_type: {}, queue_expiration: {}, queue_auto_delete: " + "{}, durable: {}. {}".format( + exchange, queue, routing_key, exchange_type, + queue_expiration, queue_auto_delete, durable, str(e) + ) + ) + + @staticmethod + def _do_publish(pool, exchange, routing_key, body, properties, + mandatory, expiration_time): + timeout = (None if expiration_time is None else + expiration_time - time.time()) + if timeout is not None and timeout < 0: + raise exceptions.MessagingTimeout( + "Timeout for current operation was expired." + ) + try: + with pool.acquire(timeout=timeout) as conn: + if timeout is not None: + properties.expiration = str(int(timeout * 1000)) + conn.channel.publish( + exchange=exchange, + routing_key=routing_key, + body=body, + properties=properties, + mandatory=mandatory + ) + except pika_exceptions.NackError as e: + raise pika_drv_exc.MessageRejectedException( + "Can not send message: [body: {}], properties: {}] to " + "target [exchange: {}, routing_key: {}]. {}".format( + body, properties, exchange, routing_key, str(e) + ) + ) + except pika_exceptions.UnroutableError as e: + raise pika_drv_exc.RoutingException( + "Can not deliver message:[body:{}, properties: {}] to any" + "queue using target: [exchange:{}, " + "routing_key:{}]. {}".format( + body, properties, exchange, routing_key, str(e) + ) + ) + except pika_pool.Timeout as e: + raise exceptions.MessagingTimeout( + "Timeout for current operation was expired. {}".format(str(e)) + ) + except pika_pool.Connection.connectivity_errors as e: + if (isinstance(e, pika_exceptions.ChannelClosed) + and e.args and e.args[0] == 404): + raise pika_drv_exc.ExchangeNotFoundException( + "Attempt to send message to not existing exchange " + "detected, message: [body:{}, properties: {}], target: " + "[exchange:{}, routing_key:{}]. {}".format( + body, properties, exchange, routing_key, str(e) + ) + ) + + raise pika_drv_exc.ConnectionException( + "Connectivity problem detected during sending the message: " + "[body:{}, properties: {}] to target: [exchange:{}, " + "routing_key:{}]. {}".format( + body, properties, exchange, routing_key, str(e) + ) + ) + except socket.timeout: + raise pika_drv_exc.TimeoutConnectionException( + "Socket timeout exceeded." + ) + + def publish(self, exchange, routing_key, body, properties, confirm, + mandatory, expiration_time, retrier): + pool = (self.connection_with_confirmation_pool if confirm else + self.connection_pool) + + LOG.debug( + "Sending message:[body:{}; properties: {}] to target: " + "[exchange:{}; routing_key:{}]".format( + body, properties, exchange, routing_key + ) + ) + + do_publish = (self._do_publish if retrier is None else + retrier(self._do_publish)) + + return do_publish(pool, exchange, routing_key, body, properties, + mandatory, expiration_time) + + def get_reply_q(self, timeout=None): + if self._reply_consumer_enabled: + return self._reply_queue + + with self._reply_consumer_lock: + if self._reply_consumer_enabled: + return self._reply_queue + + if self._reply_queue is None: + self._reply_queue = "reply.{}.{}.{}".format( + self.conf.project, self.conf.prog, uuid.uuid4().hex + ) + + if self._reply_listener is None: + self._reply_listener = pika_listener.RpcReplyPikaListener( + pika_engine=self, + exchange=self.rpc_reply_exchange, + queue=self._reply_queue, + no_ack=not self.rpc_reply_listener_ack, + prefetch_count=self.rpc_reply_listener_prefetch_count + ) + + self._reply_listener.start(timeout=timeout) + + if self._puller_thread is None: + self._puller_thread = threading.Thread(target=self._poller) + self._puller_thread.daemon = True + + if not self._puller_thread.is_alive(): + self._puller_thread.start() + + self._reply_consumer_enabled = True + + return self._reply_queue + + def _poller(self): + while self._reply_consumer_thread_run_flag: + try: + message = self._reply_listener.poll(timeout=1) + message.acknowledge() + if message is None: + continue + i = 0 + curtime = time.time() + while (i < len(self._reply_waiting_future_list) and + self._reply_consumer_thread_run_flag): + msg_id, future, expiration = ( + self._reply_waiting_future_list[i] + ) + if expiration and expiration < curtime: + del self._reply_waiting_future_list[i] + elif msg_id == message.msg_id: + del self._reply_waiting_future_list[i] + future.set_result(message) + else: + i += 1 + except BaseException: + LOG.exception("Exception during reply polling") + + def register_reply_waiter(self, msg_id, future, expiration_time): + self._reply_waiting_future_list.append( + (msg_id, future, expiration_time) + ) + + def cleanup(self): + with self._reply_consumer_lock: + self._reply_consumer_enabled = False + + if self._puller_thread: + if self._puller_thread.is_alive(): + self._reply_consumer_thread_run_flag = False + self._puller_thread.join() + self._puller_thread = None + + if self._reply_listener: + self._reply_listener.stop() + self._reply_listener.cleanup() + self._reply_listener = None + + self._reply_queue = None diff --git a/oslo_messaging/_drivers/pika_driver/pika_exceptions.py b/oslo_messaging/_drivers/pika_driver/pika_exceptions.py new file mode 100644 index 000000000..62e370fa3 --- /dev/null +++ b/oslo_messaging/_drivers/pika_driver/pika_exceptions.py @@ -0,0 +1,43 @@ +# Copyright 2015 Mirantis, 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. + +from oslo_messaging import exceptions + + +class ExchangeNotFoundException(exceptions.MessageDeliveryFailure): + pass + + +class MessageRejectedException(exceptions.MessageDeliveryFailure): + pass + + +class RoutingException(exceptions.MessageDeliveryFailure): + pass + + +class ConnectionException(exceptions.MessagingException): + pass + + +class HostConnectionNotAllowedException(ConnectionException): + pass + + +class EstablishConnectionException(ConnectionException): + pass + + +class TimeoutConnectionException(ConnectionException): + pass diff --git a/oslo_messaging/_drivers/pika_driver/pika_listener.py b/oslo_messaging/_drivers/pika_driver/pika_listener.py new file mode 100644 index 000000000..08d17ea24 --- /dev/null +++ b/oslo_messaging/_drivers/pika_driver/pika_listener.py @@ -0,0 +1,266 @@ +# Copyright 2015 Mirantis, 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. + +import collections + +from oslo_log import log as logging + +import threading +import time + +from oslo_messaging._drivers.pika_driver import pika_message +import pika_pool +import retrying + +LOG = logging.getLogger(__name__) + + +class PikaListener(object): + def __init__(self, pika_engine, no_ack, prefetch_count): + self._pika_engine = pika_engine + + self._connection = None + self._channel = None + self._lock = threading.Lock() + + self._prefetch_count = prefetch_count + self._no_ack = no_ack + + self._started = False + + self._message_queue = collections.deque() + + def _reconnect(self): + self._connection = self._pika_engine.create_connection( + for_listening=True + ) + self._channel = self._connection.channel() + self._channel.basic_qos(prefetch_count=self._prefetch_count) + + self._on_reconnected() + + def _on_reconnected(self): + raise NotImplementedError( + "It is base class. Please declare consumers here" + ) + + def _start_consuming(self, queue): + self._channel.basic_consume(self._on_message_callback, + queue, no_ack=self._no_ack) + + def _on_message_callback(self, unused, method, properties, body): + self._message_queue.append((self._channel, method, properties, body)) + + def _cleanup(self): + if self._channel: + try: + self._channel.close() + except Exception as ex: + if not pika_pool.Connection.is_connection_invalidated(ex): + LOG.exception("Unexpected error during closing channel") + self._channel = None + + if self._connection: + try: + self._connection.close() + except Exception as ex: + if not pika_pool.Connection.is_connection_invalidated(ex): + LOG.exception("Unexpected error during closing connection") + self._connection = None + + def poll(self, timeout=None): + start = time.time() + while not self._message_queue: + with self._lock: + if not self._started: + return None + + try: + if self._channel is None: + self._reconnect() + self._connection.process_data_events() + except Exception: + self._cleanup() + raise + if timeout and time.time() - start > timeout: + return None + + return self._message_queue.popleft() + + def start(self): + self._started = True + + def stop(self): + with self._lock: + if not self._started: + return + + self._started = False + self._cleanup() + + def reconnect(self): + with self._lock: + self._cleanup() + try: + self._reconnect() + except Exception: + self._cleanup() + raise + + def cleanup(self): + with self._lock: + self._cleanup() + + +class RpcServicePikaListener(PikaListener): + def __init__(self, pika_engine, target, no_ack, prefetch_count): + self._target = target + + super(RpcServicePikaListener, self).__init__( + pika_engine, no_ack=no_ack, prefetch_count=prefetch_count) + + def _on_reconnected(self): + exchange = (self._target.exchange or + self._pika_engine.default_rpc_exchange) + queue = '{}'.format(self._target.topic) + server_queue = '{}.{}'.format(queue, self._target.server) + + fanout_exchange = '{}_fanout_{}'.format( + self._pika_engine.default_rpc_exchange, self._target.topic + ) + + queue_expiration = ( + self._pika_engine.conf.oslo_messaging_pika.rpc_queue_expiration + ) + + self._pika_engine.declare_queue_binding_by_channel( + channel=self._channel, + exchange=exchange, queue=queue, routing_key=queue, + exchange_type='direct', queue_expiration=queue_expiration, + queue_auto_delete=False, durable=False + ) + self._pika_engine.declare_queue_binding_by_channel( + channel=self._channel, + exchange=exchange, queue=server_queue, routing_key=server_queue, + exchange_type='direct', queue_expiration=queue_expiration, + queue_auto_delete=False, durable=False + ) + self._pika_engine.declare_queue_binding_by_channel( + channel=self._channel, + exchange=fanout_exchange, queue=server_queue, routing_key="", + exchange_type='fanout', queue_expiration=queue_expiration, + queue_auto_delete=False, durable=False + ) + + self._start_consuming(queue) + self._start_consuming(server_queue) + + def poll(self, timeout=None): + msg = super(RpcServicePikaListener, self).poll(timeout) + if msg is None: + return None + return pika_message.PikaIncomingMessage( + self._pika_engine, *msg, no_ack=self._no_ack + ) + + +class RpcReplyPikaListener(PikaListener): + def __init__(self, pika_engine, exchange, queue, no_ack, prefetch_count): + self._exchange = exchange + self._queue = queue + + super(RpcReplyPikaListener, self).__init__( + pika_engine, no_ack, prefetch_count + ) + + def _on_reconnected(self): + queue_expiration = ( + self._pika_engine.conf.oslo_messaging_pika.rpc_queue_expiration + ) + + self._pika_engine.declare_queue_binding_by_channel( + channel=self._channel, + exchange=self._exchange, queue=self._queue, + routing_key=self._queue, exchange_type='direct', + queue_expiration=queue_expiration, queue_auto_delete=False, + durable=False + ) + self._start_consuming(self._queue) + + def start(self, timeout=None): + super(RpcReplyPikaListener, self).start() + + def on_exception(ex): + LOG.warn(str(ex)) + + return True + + retrier = retrying.retry( + stop_max_attempt_number=self._pika_engine.rpc_reply_retry_attempts, + stop_max_delay=timeout * 1000, + wait_fixed=self._pika_engine.rpc_reply_retry_delay * 1000, + retry_on_exception=on_exception, + ) + + retrier(self.reconnect)() + + def poll(self, timeout=None): + msg = super(RpcReplyPikaListener, self).poll(timeout) + if msg is None: + return None + return pika_message.PikaIncomingMessage( + self._pika_engine, *msg, no_ack=self._no_ack + ) + + +class NotificationPikaListener(PikaListener): + def __init__(self, pika_engine, targets_and_priorities, + queue_name=None, prefetch_count=100): + self._targets_and_priorities = targets_and_priorities + self._queue_name = queue_name + + super(NotificationPikaListener, self).__init__( + pika_engine, no_ack=False, prefetch_count=prefetch_count + ) + + def _on_reconnected(self): + queues_to_consume = set() + for target, priority in self._targets_and_priorities: + routing_key = '%s.%s' % (target.topic, priority) + queue = self._queue_name or routing_key + self._pika_engine.declare_queue_binding_by_channel( + channel=self._channel, + exchange=( + target.exchange or + self._pika_engine.default_notification_exchange + ), + queue = queue, + routing_key=routing_key, + exchange_type='direct', + queue_expiration=None, + queue_auto_delete=False, + durable=self._pika_engine.notification_persistence, + ) + queues_to_consume.add(queue) + + for queue_to_consume in queues_to_consume: + self._start_consuming(queue_to_consume) + + def poll(self, timeout=None): + msg = super(NotificationPikaListener, self).poll(timeout) + if msg is None: + return None + return pika_message.PikaIncomingMessage( + self._pika_engine, *msg, no_ack=self._no_ack + ) diff --git a/oslo_messaging/_drivers/pika_driver/pika_message.py b/oslo_messaging/_drivers/pika_driver/pika_message.py new file mode 100644 index 000000000..09559b8a0 --- /dev/null +++ b/oslo_messaging/_drivers/pika_driver/pika_message.py @@ -0,0 +1,217 @@ +# Copyright 2015 Mirantis, 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. +from concurrent import futures + +from oslo_log import log as logging + +from oslo_messaging._drivers import common +from oslo_messaging import exceptions + +from oslo_messaging._drivers.pika_driver import pika_exceptions as pika_drv_exc + +from oslo_serialization import jsonutils + +from pika import spec as pika_spec + +import retrying +import six +import time +import uuid + +LOG = logging.getLogger(__name__) + + +class PikaIncomingMessage(object): + + def __init__(self, pika_engine, channel, method, properties, body, no_ack): + self._pika_engine = pika_engine + self._no_ack = no_ack + self._channel = channel + self.delivery_tag = method.delivery_tag + + self.content_type = getattr(properties, "content_type", + "application/json") + self.content_encoding = getattr(properties, "content_encoding", + "utf-8") + + self.expiration_time = ( + None if properties.expiration is None else + time.time() + int(properties.expiration) / 1000 + ) + + if self.content_type != "application/json": + raise NotImplementedError("Content-type['{}'] is not valid, " + "'application/json' only is supported.") + + message_dict = common.deserialize_msg( + jsonutils.loads(body, encoding=self.content_encoding) + ) + + self.unique_id = message_dict.pop('_unique_id') + self.msg_id = message_dict.pop('_msg_id', None) + self.reply_q = message_dict.pop('_reply_q', None) + + context_dict = {} + + for key in list(message_dict.keys()): + key = six.text_type(key) + if key.startswith('_context_'): + value = message_dict.pop(key) + context_dict[key[9:]] = value + + self.message = message_dict + self.ctxt = context_dict + + def reply(self, reply=None, failure=None, log_failure=True): + if not (self.msg_id and self.reply_q): + return + + if failure: + failure = common.serialize_remote_exception(failure, log_failure) + + msg = { + 'result': reply, + 'failure': failure, + '_unique_id': uuid.uuid4().hex, + '_msg_id': self.msg_id, + 'ending': True + } + + def on_exception(ex): + if isinstance(ex, pika_drv_exc.ConnectionException): + LOG.warn(str(ex)) + return True + else: + return False + + retrier = retrying.retry( + stop_max_attempt_number=( + None if self._pika_engine.rpc_reply_retry_attempts == -1 + else self._pika_engine.rpc_reply_retry_attempts + ), + retry_on_exception=on_exception, + wait_fixed=self._pika_engine.rpc_reply_retry_delay * 1000, + ) + + try: + self._pika_engine.publish( + exchange=self._pika_engine.rpc_reply_exchange, + routing_key=self.reply_q, + body=jsonutils.dumps( + common.serialize_msg(msg), + encoding=self.content_encoding + ), + properties=pika_spec.BasicProperties( + content_encoding=self.content_encoding, + content_type=self.content_type, + ), + confirm=True, + mandatory=False, + expiration_time=self.expiration_time, + retrier=retrier + ) + LOG.debug( + "Message [id:'{}'] replied to '{}'.".format( + self.msg_id, self.reply_q + ) + ) + except Exception: + LOG.exception( + "Message [id:'{}'] wasn't replied to : {}".format( + self.msg_id, self.reply_q + ) + ) + + def acknowledge(self): + if not self._no_ack: + self._channel.basic_ack(delivery_tag=self.delivery_tag) + + def requeue(self): + if not self._no_ack: + return self._channel.basic_nack(delivery_tag=self.delivery_tag, + requeue=True) + + +class PikaOutgoingMessage(object): + + def __init__(self, pika_engine, message, context, + content_type="application/json", content_encoding="utf-8"): + self._pika_engine = pika_engine + + self.content_type = content_type + self.content_encoding = content_encoding + + if self.content_type != "application/json": + raise NotImplementedError("Content-type['{}'] is not valid, " + "'application/json' only is supported.") + + self.message = message + self.context = context + + self.unique_id = uuid.uuid4().hex + self.msg_id = None + + def send(self, exchange, routing_key='', confirm=True, + wait_for_reply=False, mandatory=True, persistent=False, + timeout=None, retrier=None): + msg = self.message.copy() + + msg['_unique_id'] = self.unique_id + + for key, value in self.context.iteritems(): + key = six.text_type(key) + msg['_context_' + key] = value + + properties = pika_spec.BasicProperties( + content_encoding=self.content_encoding, + content_type=self.content_type, + delivery_mode=2 if persistent else 1 + ) + + expiration_time = ( + None if timeout is None else (timeout + time.time()) + ) + + if wait_for_reply: + self.msg_id = uuid.uuid4().hex + msg['_msg_id'] = self.msg_id + LOG.debug('MSG_ID is %s', self.msg_id) + + msg['_reply_q'] = self._pika_engine.get_reply_q(timeout) + + future = futures.Future() + + self._pika_engine.register_reply_waiter( + msg_id=self.msg_id, future=future, + expiration_time=expiration_time + ) + + self._pika_engine.publish( + exchange=exchange, routing_key=routing_key, + body=jsonutils.dumps( + common.serialize_msg(msg), + encoding=self.content_encoding + ), + properties=properties, + confirm=confirm, + mandatory=mandatory, + expiration_time=expiration_time, + retrier=retrier + ) + + if wait_for_reply: + try: + return future.result(timeout) + except futures.TimeoutError: + raise exceptions.MessagingTimeout() From cc3db22c6a888274c9daf73dda9d64f23bf2d030 Mon Sep 17 00:00:00 2001 From: Dmitriy Ukhlov Date: Wed, 11 Nov 2015 18:54:37 +0200 Subject: [PATCH 06/16] Implements more smart retrying This patch makes difference between messages which are sent with retry=0 and retry!=0. In first case we consider that request could be not idempotent and we should guarantee that such messages we be not sent more then one time. Therefore we use consuming without acknowledgements for such messages and consuming with acknowledgements for messages which could be processed twice to avoid message loses Change-Id: I7dc1705e84dc6a7671bf1f379d4ef4980e41491e --- oslo_messaging/_drivers/impl_pika.py | 65 ++-- .../_drivers/pika_driver/pika_engine.py | 194 +---------- .../_drivers/pika_driver/pika_listener.py | 321 +++++------------- .../_drivers/pika_driver/pika_message.py | 223 +++++++++--- .../_drivers/pika_driver/pika_poller.py | 293 ++++++++++++++++ 5 files changed, 588 insertions(+), 508 deletions(-) create mode 100644 oslo_messaging/_drivers/pika_driver/pika_poller.py diff --git a/oslo_messaging/_drivers/impl_pika.py b/oslo_messaging/_drivers/impl_pika.py index 085901a16..b4b6d4288 100644 --- a/oslo_messaging/_drivers/impl_pika.py +++ b/oslo_messaging/_drivers/impl_pika.py @@ -13,17 +13,19 @@ # under the License. import retrying - import sys +import time from oslo_config import cfg from oslo_log import log as logging from oslo_messaging._drivers import common -from oslo_messaging._drivers.pika_driver import pika_engine + +from oslo_messaging._drivers.pika_driver import pika_engine as pika_drv_engine from oslo_messaging._drivers.pika_driver import pika_exceptions as pika_drv_exc -from oslo_messaging._drivers.pika_driver import pika_listener -from oslo_messaging._drivers.pika_driver import pika_message +from oslo_messaging._drivers.pika_driver import pika_listener as pika_drv_lstnr +from oslo_messaging._drivers.pika_driver import pika_message as pika_drv_msg +from oslo_messaging._drivers.pika_driver import pika_poller as pika_drv_poller from oslo_messaging import exceptions @@ -95,15 +97,6 @@ rpc_opts = [ help="Exchange name for for sending RPC messages"), cfg.StrOpt('rpc_reply_exchange', default="${control_exchange}_rpc_reply", help="Exchange name for for receiving RPC replies"), - - cfg.BoolOpt('rpc_listener_ack', default=True, - help="Disable to increase performance. If disabled - some " - "messages may be lost in case of connectivity problem. " - "If enabled - may cause not needed message redelivery " - "and rpc request could be processed more then one time"), - cfg.BoolOpt('rpc_reply_listener_ack', default=True, - help="Disable to increase performance. If disabled - some " - "replies may be lost in case of connectivity problem."), cfg.IntOpt( 'rpc_listener_prefetch_count', default=10, help="Max number of not acknowledged message which RabbitMQ can send " @@ -176,13 +169,19 @@ class PikaDriver(object): self.conf = conf self._allowed_remote_exmods = allowed_remote_exmods - self._pika_engine = pika_engine.PikaEngine(conf, url, default_exchange) + self._pika_engine = pika_drv_engine.PikaEngine( + conf, url, default_exchange + ) + self._reply_listener = pika_drv_lstnr.RpcReplyPikaListener( + self._pika_engine + ) def require_features(self, requeue=False): pass def send(self, target, ctxt, message, wait_for_reply=None, timeout=None, retry=None): + expiration_time = None if timeout is None else time.time() + timeout if retry is None: retry = self._pika_engine.default_rpc_retry_attempts @@ -204,29 +203,12 @@ class PikaDriver(object): ) ) - msg = pika_message.PikaOutgoingMessage(self._pika_engine, message, - ctxt) - - if target.fanout: - return msg.send( - exchange='{}_fanout_{}'.format( - self._pika_engine.default_rpc_exchange, target.topic - ), - timeout=timeout, confirm=True, mandatory=False, - retrier=retrier - ) - - queue = target.topic - if target.server: - queue = '{}.{}'.format(queue, target.server) - + msg = pika_drv_msg.RpcPikaOutgoingMessage(self._pika_engine, message, + ctxt) reply = msg.send( - exchange=target.exchange or self._pika_engine.default_rpc_exchange, - routing_key=queue, - wait_for_reply=wait_for_reply, - timeout=timeout, - confirm=True, - mandatory=True, + target, + reply_listener=self._reply_listener if wait_for_reply else None, + expiration_time=expiration_time, retrier=retrier ) @@ -276,16 +258,14 @@ class PikaDriver(object): wait_fixed=self._pika_engine.notification_retry_delay * 1000, ) - msg = pika_message.PikaOutgoingMessage(self._pika_engine, message, + msg = pika_drv_msg.PikaOutgoingMessage(self._pika_engine, message, ctxt) - return msg.send( exchange=( target.exchange or self._pika_engine.default_notification_exchange ), routing_key=target.topic, - wait_for_reply=False, confirm=True, mandatory=True, persistent=self._pika_engine.notification_persistence, @@ -293,23 +273,22 @@ class PikaDriver(object): ) def listen(self, target): - listener = pika_listener.RpcServicePikaListener( + listener = pika_drv_poller.RpcServicePikaPoller( self._pika_engine, target, - no_ack=not self._pika_engine.rpc_listener_ack, prefetch_count=self._pika_engine.rpc_listener_prefetch_count ) listener.start() return listener def listen_for_notifications(self, targets_and_priorities, pool): - listener = pika_listener.NotificationPikaListener( + listener = pika_drv_poller.NotificationPikaPoller( self._pika_engine, targets_and_priorities, pool ) listener.start() return listener def cleanup(self): - self._pika_engine.cleanup() + self._reply_listener.cleanup() class PikaDriverCompatibleWithRabbitDriver(PikaDriver): diff --git a/oslo_messaging/_drivers/pika_driver/pika_engine.py b/oslo_messaging/_drivers/pika_driver/pika_engine.py index 991824d64..03ccccdf0 100644 --- a/oslo_messaging/_drivers/pika_driver/pika_engine.py +++ b/oslo_messaging/_drivers/pika_driver/pika_engine.py @@ -18,18 +18,15 @@ from oslo_log import log as logging from oslo_messaging import exceptions from oslo_messaging._drivers.pika_driver import pika_exceptions as pika_drv_exc -from oslo_messaging._drivers.pika_driver import pika_listener import pika from pika import adapters as pika_adapters from pika import credentials as pika_credentials -from pika import exceptions as pika_exceptions import pika_pool import threading import time -import uuid LOG = logging.getLogger(__name__) @@ -65,12 +62,6 @@ class PikaEngine(object): default_exchange ) - self.rpc_listener_ack = conf.oslo_messaging_pika.rpc_listener_ack - - self.rpc_reply_listener_ack = ( - conf.oslo_messaging_pika.rpc_reply_listener_ack - ) - self.rpc_listener_prefetch_count = ( conf.oslo_messaging_pika.rpc_listener_prefetch_count ) @@ -129,17 +120,6 @@ class PikaEngine(object): raise ValueError("notification_retry_delay should be non-negative " "integer") - # preparing poller for listening replies - self._reply_queue = None - - self._reply_listener = None - self._reply_waiting_future_list = [] - - self._reply_consumer_enabled = False - self._reply_consumer_thread_run_flag = True - self._reply_consumer_lock = threading.Lock() - self._puller_thread = None - self._tcp_user_timeout = self.conf.oslo_messaging_pika.tcp_user_timeout self._host_connection_reconnect_delay = ( self.conf.oslo_messaging_pika.host_connection_reconnect_delay @@ -348,163 +328,19 @@ class PikaEngine(object): ) ) + def get_rpc_exchange_name(self, exchange, topic, fanout, no_ack): + exchange = (exchange or self.default_rpc_exchange) + + if fanout: + exchange = '{}_fanout_{}_{}'.format( + exchange, "no_ack" if no_ack else "with_ack", topic + ) + return exchange + @staticmethod - def _do_publish(pool, exchange, routing_key, body, properties, - mandatory, expiration_time): - timeout = (None if expiration_time is None else - expiration_time - time.time()) - if timeout is not None and timeout < 0: - raise exceptions.MessagingTimeout( - "Timeout for current operation was expired." - ) - try: - with pool.acquire(timeout=timeout) as conn: - if timeout is not None: - properties.expiration = str(int(timeout * 1000)) - conn.channel.publish( - exchange=exchange, - routing_key=routing_key, - body=body, - properties=properties, - mandatory=mandatory - ) - except pika_exceptions.NackError as e: - raise pika_drv_exc.MessageRejectedException( - "Can not send message: [body: {}], properties: {}] to " - "target [exchange: {}, routing_key: {}]. {}".format( - body, properties, exchange, routing_key, str(e) - ) - ) - except pika_exceptions.UnroutableError as e: - raise pika_drv_exc.RoutingException( - "Can not deliver message:[body:{}, properties: {}] to any" - "queue using target: [exchange:{}, " - "routing_key:{}]. {}".format( - body, properties, exchange, routing_key, str(e) - ) - ) - except pika_pool.Timeout as e: - raise exceptions.MessagingTimeout( - "Timeout for current operation was expired. {}".format(str(e)) - ) - except pika_pool.Connection.connectivity_errors as e: - if (isinstance(e, pika_exceptions.ChannelClosed) - and e.args and e.args[0] == 404): - raise pika_drv_exc.ExchangeNotFoundException( - "Attempt to send message to not existing exchange " - "detected, message: [body:{}, properties: {}], target: " - "[exchange:{}, routing_key:{}]. {}".format( - body, properties, exchange, routing_key, str(e) - ) - ) - - raise pika_drv_exc.ConnectionException( - "Connectivity problem detected during sending the message: " - "[body:{}, properties: {}] to target: [exchange:{}, " - "routing_key:{}]. {}".format( - body, properties, exchange, routing_key, str(e) - ) - ) - except socket.timeout: - raise pika_drv_exc.TimeoutConnectionException( - "Socket timeout exceeded." - ) - - def publish(self, exchange, routing_key, body, properties, confirm, - mandatory, expiration_time, retrier): - pool = (self.connection_with_confirmation_pool if confirm else - self.connection_pool) - - LOG.debug( - "Sending message:[body:{}; properties: {}] to target: " - "[exchange:{}; routing_key:{}]".format( - body, properties, exchange, routing_key - ) - ) - - do_publish = (self._do_publish if retrier is None else - retrier(self._do_publish)) - - return do_publish(pool, exchange, routing_key, body, properties, - mandatory, expiration_time) - - def get_reply_q(self, timeout=None): - if self._reply_consumer_enabled: - return self._reply_queue - - with self._reply_consumer_lock: - if self._reply_consumer_enabled: - return self._reply_queue - - if self._reply_queue is None: - self._reply_queue = "reply.{}.{}.{}".format( - self.conf.project, self.conf.prog, uuid.uuid4().hex - ) - - if self._reply_listener is None: - self._reply_listener = pika_listener.RpcReplyPikaListener( - pika_engine=self, - exchange=self.rpc_reply_exchange, - queue=self._reply_queue, - no_ack=not self.rpc_reply_listener_ack, - prefetch_count=self.rpc_reply_listener_prefetch_count - ) - - self._reply_listener.start(timeout=timeout) - - if self._puller_thread is None: - self._puller_thread = threading.Thread(target=self._poller) - self._puller_thread.daemon = True - - if not self._puller_thread.is_alive(): - self._puller_thread.start() - - self._reply_consumer_enabled = True - - return self._reply_queue - - def _poller(self): - while self._reply_consumer_thread_run_flag: - try: - message = self._reply_listener.poll(timeout=1) - message.acknowledge() - if message is None: - continue - i = 0 - curtime = time.time() - while (i < len(self._reply_waiting_future_list) and - self._reply_consumer_thread_run_flag): - msg_id, future, expiration = ( - self._reply_waiting_future_list[i] - ) - if expiration and expiration < curtime: - del self._reply_waiting_future_list[i] - elif msg_id == message.msg_id: - del self._reply_waiting_future_list[i] - future.set_result(message) - else: - i += 1 - except BaseException: - LOG.exception("Exception during reply polling") - - def register_reply_waiter(self, msg_id, future, expiration_time): - self._reply_waiting_future_list.append( - (msg_id, future, expiration_time) - ) - - def cleanup(self): - with self._reply_consumer_lock: - self._reply_consumer_enabled = False - - if self._puller_thread: - if self._puller_thread.is_alive(): - self._reply_consumer_thread_run_flag = False - self._puller_thread.join() - self._puller_thread = None - - if self._reply_listener: - self._reply_listener.stop() - self._reply_listener.cleanup() - self._reply_listener = None - - self._reply_queue = None + def get_rpc_queue_name(topic, server, no_ack): + queue_parts = ["no_ack" if no_ack else "with_ack", topic] + if server is not None: + queue_parts.append(server) + queue = '.'.join(queue_parts) + return queue diff --git a/oslo_messaging/_drivers/pika_driver/pika_listener.py b/oslo_messaging/_drivers/pika_driver/pika_listener.py index 08d17ea24..493adf1ea 100644 --- a/oslo_messaging/_drivers/pika_driver/pika_listener.py +++ b/oslo_messaging/_drivers/pika_driver/pika_listener.py @@ -12,255 +12,112 @@ # License for the specific language governing permissions and limitations # under the License. -import collections - from oslo_log import log as logging +from oslo_messaging._drivers.pika_driver import pika_poller as pika_drv_poller + + import threading import time - -from oslo_messaging._drivers.pika_driver import pika_message -import pika_pool -import retrying +import uuid LOG = logging.getLogger(__name__) -class PikaListener(object): - def __init__(self, pika_engine, no_ack, prefetch_count): +class RpcReplyPikaListener(object): + def __init__(self, pika_engine): self._pika_engine = pika_engine - self._connection = None - self._channel = None - self._lock = threading.Lock() + # preparing poller for listening replies + self._reply_queue = None - self._prefetch_count = prefetch_count - self._no_ack = no_ack + self._reply_poller = None + self._reply_waiting_future_list = [] - self._started = False + self._reply_consumer_enabled = False + self._reply_consumer_thread_run_flag = True + self._reply_consumer_lock = threading.Lock() + self._puller_thread = None - self._message_queue = collections.deque() + def get_reply_qname(self, expiration_time=None): + if self._reply_consumer_enabled: + return self._reply_queue - def _reconnect(self): - self._connection = self._pika_engine.create_connection( - for_listening=True + with self._reply_consumer_lock: + if self._reply_consumer_enabled: + return self._reply_queue + + if self._reply_queue is None: + self._reply_queue = "reply.{}.{}.{}".format( + self._pika_engine.conf.project, + self._pika_engine.conf.prog, uuid.uuid4().hex + ) + + if self._reply_poller is None: + self._reply_poller = pika_drv_poller.RpcReplyPikaPoller( + pika_engine=self._pika_engine, + exchange=self._pika_engine.rpc_reply_exchange, + queue=self._reply_queue, + prefetch_count=( + self._pika_engine.rpc_reply_listener_prefetch_count + ) + ) + + self._reply_poller.start(timeout=expiration_time - time.time()) + + if self._puller_thread is None: + self._puller_thread = threading.Thread(target=self._poller) + self._puller_thread.daemon = True + + if not self._puller_thread.is_alive(): + self._puller_thread.start() + + self._reply_consumer_enabled = True + + return self._reply_queue + + def _poller(self): + while self._reply_consumer_thread_run_flag: + try: + message = self._reply_poller.poll(timeout=1) + if message is None: + continue + message.acknowledge() + i = 0 + curtime = time.time() + while (i < len(self._reply_waiting_future_list) and + self._reply_consumer_thread_run_flag): + msg_id, future, expiration = ( + self._reply_waiting_future_list[i] + ) + if expiration and expiration < curtime: + del self._reply_waiting_future_list[i] + elif msg_id == message.msg_id: + del self._reply_waiting_future_list[i] + future.set_result(message) + else: + i += 1 + except BaseException: + LOG.exception("Exception during reply polling") + + def register_reply_waiter(self, msg_id, future, expiration_time): + self._reply_waiting_future_list.append( + (msg_id, future, expiration_time) ) - self._channel = self._connection.channel() - self._channel.basic_qos(prefetch_count=self._prefetch_count) - - self._on_reconnected() - - def _on_reconnected(self): - raise NotImplementedError( - "It is base class. Please declare consumers here" - ) - - def _start_consuming(self, queue): - self._channel.basic_consume(self._on_message_callback, - queue, no_ack=self._no_ack) - - def _on_message_callback(self, unused, method, properties, body): - self._message_queue.append((self._channel, method, properties, body)) - - def _cleanup(self): - if self._channel: - try: - self._channel.close() - except Exception as ex: - if not pika_pool.Connection.is_connection_invalidated(ex): - LOG.exception("Unexpected error during closing channel") - self._channel = None - - if self._connection: - try: - self._connection.close() - except Exception as ex: - if not pika_pool.Connection.is_connection_invalidated(ex): - LOG.exception("Unexpected error during closing connection") - self._connection = None - - def poll(self, timeout=None): - start = time.time() - while not self._message_queue: - with self._lock: - if not self._started: - return None - - try: - if self._channel is None: - self._reconnect() - self._connection.process_data_events() - except Exception: - self._cleanup() - raise - if timeout and time.time() - start > timeout: - return None - - return self._message_queue.popleft() - - def start(self): - self._started = True - - def stop(self): - with self._lock: - if not self._started: - return - - self._started = False - self._cleanup() - - def reconnect(self): - with self._lock: - self._cleanup() - try: - self._reconnect() - except Exception: - self._cleanup() - raise def cleanup(self): - with self._lock: - self._cleanup() + with self._reply_consumer_lock: + self._reply_consumer_enabled = False + if self._puller_thread: + if self._puller_thread.is_alive(): + self._reply_consumer_thread_run_flag = False + self._puller_thread.join() + self._puller_thread = None -class RpcServicePikaListener(PikaListener): - def __init__(self, pika_engine, target, no_ack, prefetch_count): - self._target = target + if self._reply_poller: + self._reply_poller.stop() + self._reply_poller.cleanup() + self._reply_poller = None - super(RpcServicePikaListener, self).__init__( - pika_engine, no_ack=no_ack, prefetch_count=prefetch_count) - - def _on_reconnected(self): - exchange = (self._target.exchange or - self._pika_engine.default_rpc_exchange) - queue = '{}'.format(self._target.topic) - server_queue = '{}.{}'.format(queue, self._target.server) - - fanout_exchange = '{}_fanout_{}'.format( - self._pika_engine.default_rpc_exchange, self._target.topic - ) - - queue_expiration = ( - self._pika_engine.conf.oslo_messaging_pika.rpc_queue_expiration - ) - - self._pika_engine.declare_queue_binding_by_channel( - channel=self._channel, - exchange=exchange, queue=queue, routing_key=queue, - exchange_type='direct', queue_expiration=queue_expiration, - queue_auto_delete=False, durable=False - ) - self._pika_engine.declare_queue_binding_by_channel( - channel=self._channel, - exchange=exchange, queue=server_queue, routing_key=server_queue, - exchange_type='direct', queue_expiration=queue_expiration, - queue_auto_delete=False, durable=False - ) - self._pika_engine.declare_queue_binding_by_channel( - channel=self._channel, - exchange=fanout_exchange, queue=server_queue, routing_key="", - exchange_type='fanout', queue_expiration=queue_expiration, - queue_auto_delete=False, durable=False - ) - - self._start_consuming(queue) - self._start_consuming(server_queue) - - def poll(self, timeout=None): - msg = super(RpcServicePikaListener, self).poll(timeout) - if msg is None: - return None - return pika_message.PikaIncomingMessage( - self._pika_engine, *msg, no_ack=self._no_ack - ) - - -class RpcReplyPikaListener(PikaListener): - def __init__(self, pika_engine, exchange, queue, no_ack, prefetch_count): - self._exchange = exchange - self._queue = queue - - super(RpcReplyPikaListener, self).__init__( - pika_engine, no_ack, prefetch_count - ) - - def _on_reconnected(self): - queue_expiration = ( - self._pika_engine.conf.oslo_messaging_pika.rpc_queue_expiration - ) - - self._pika_engine.declare_queue_binding_by_channel( - channel=self._channel, - exchange=self._exchange, queue=self._queue, - routing_key=self._queue, exchange_type='direct', - queue_expiration=queue_expiration, queue_auto_delete=False, - durable=False - ) - self._start_consuming(self._queue) - - def start(self, timeout=None): - super(RpcReplyPikaListener, self).start() - - def on_exception(ex): - LOG.warn(str(ex)) - - return True - - retrier = retrying.retry( - stop_max_attempt_number=self._pika_engine.rpc_reply_retry_attempts, - stop_max_delay=timeout * 1000, - wait_fixed=self._pika_engine.rpc_reply_retry_delay * 1000, - retry_on_exception=on_exception, - ) - - retrier(self.reconnect)() - - def poll(self, timeout=None): - msg = super(RpcReplyPikaListener, self).poll(timeout) - if msg is None: - return None - return pika_message.PikaIncomingMessage( - self._pika_engine, *msg, no_ack=self._no_ack - ) - - -class NotificationPikaListener(PikaListener): - def __init__(self, pika_engine, targets_and_priorities, - queue_name=None, prefetch_count=100): - self._targets_and_priorities = targets_and_priorities - self._queue_name = queue_name - - super(NotificationPikaListener, self).__init__( - pika_engine, no_ack=False, prefetch_count=prefetch_count - ) - - def _on_reconnected(self): - queues_to_consume = set() - for target, priority in self._targets_and_priorities: - routing_key = '%s.%s' % (target.topic, priority) - queue = self._queue_name or routing_key - self._pika_engine.declare_queue_binding_by_channel( - channel=self._channel, - exchange=( - target.exchange or - self._pika_engine.default_notification_exchange - ), - queue = queue, - routing_key=routing_key, - exchange_type='direct', - queue_expiration=None, - queue_auto_delete=False, - durable=self._pika_engine.notification_persistence, - ) - queues_to_consume.add(queue) - - for queue_to_consume in queues_to_consume: - self._start_consuming(queue_to_consume) - - def poll(self, timeout=None): - msg = super(NotificationPikaListener, self).poll(timeout) - if msg is None: - return None - return pika_message.PikaIncomingMessage( - self._pika_engine, *msg, no_ack=self._no_ack - ) + self._reply_queue = None diff --git a/oslo_messaging/_drivers/pika_driver/pika_message.py b/oslo_messaging/_drivers/pika_driver/pika_message.py index 09559b8a0..f771b7cab 100644 --- a/oslo_messaging/_drivers/pika_driver/pika_message.py +++ b/oslo_messaging/_drivers/pika_driver/pika_message.py @@ -22,10 +22,13 @@ from oslo_messaging._drivers.pika_driver import pika_exceptions as pika_drv_exc from oslo_serialization import jsonutils +from pika import exceptions as pika_exceptions from pika import spec as pika_spec +import pika_pool import retrying import six +import socket import time import uuid @@ -47,7 +50,7 @@ class PikaIncomingMessage(object): self.expiration_time = ( None if properties.expiration is None else - time.time() + int(properties.expiration) / 1000 + time.time() + float(properties.expiration) / 1000 ) if self.content_type != "application/json": @@ -58,10 +61,6 @@ class PikaIncomingMessage(object): jsonutils.loads(body, encoding=self.content_encoding) ) - self.unique_id = message_dict.pop('_unique_id') - self.msg_id = message_dict.pop('_msg_id', None) - self.reply_q = message_dict.pop('_reply_q', None) - context_dict = {} for key in list(message_dict.keys()): @@ -69,10 +68,31 @@ class PikaIncomingMessage(object): if key.startswith('_context_'): value = message_dict.pop(key) context_dict[key[9:]] = value - + elif key.startswith('_'): + value = message_dict.pop(key) + setattr(self, key[1:], value) self.message = message_dict self.ctxt = context_dict + def acknowledge(self): + if not self._no_ack: + self._channel.basic_ack(delivery_tag=self.delivery_tag) + + def requeue(self): + if not self._no_ack: + return self._channel.basic_nack(delivery_tag=self.delivery_tag, + requeue=True) + + +class RpcPikaIncomingMessage(PikaIncomingMessage): + def __init__(self, pika_engine, channel, method, properties, body, no_ack): + self.msg_id = None + self.reply_q = None + + super(RpcPikaIncomingMessage, self).__init__( + pika_engine, channel, method, properties, body, no_ack + ) + def reply(self, reply=None, failure=None, log_failure=True): if not (self.msg_id and self.reply_q): return @@ -83,11 +103,14 @@ class PikaIncomingMessage(object): msg = { 'result': reply, 'failure': failure, - '_unique_id': uuid.uuid4().hex, '_msg_id': self.msg_id, - 'ending': True } + reply_outgoing_message = PikaOutgoingMessage( + self._pika_engine, msg, self.ctxt, content_type=self.content_type, + content_encoding=self.content_encoding + ) + def on_exception(ex): if isinstance(ex, pika_drv_exc.ConnectionException): LOG.warn(str(ex)) @@ -105,19 +128,12 @@ class PikaIncomingMessage(object): ) try: - self._pika_engine.publish( + reply_outgoing_message.send( exchange=self._pika_engine.rpc_reply_exchange, routing_key=self.reply_q, - body=jsonutils.dumps( - common.serialize_msg(msg), - encoding=self.content_encoding - ), - properties=pika_spec.BasicProperties( - content_encoding=self.content_encoding, - content_type=self.content_type, - ), confirm=True, mandatory=False, + persistent=False, expiration_time=self.expiration_time, retrier=retrier ) @@ -133,18 +149,8 @@ class PikaIncomingMessage(object): ) ) - def acknowledge(self): - if not self._no_ack: - self._channel.basic_ack(delivery_tag=self.delivery_tag) - - def requeue(self): - if not self._no_ack: - return self._channel.basic_nack(delivery_tag=self.delivery_tag, - requeue=True) - class PikaOutgoingMessage(object): - def __init__(self, pika_engine, message, context, content_type="application/json", content_encoding="utf-8"): self._pika_engine = pika_engine @@ -160,11 +166,8 @@ class PikaOutgoingMessage(object): self.context = context self.unique_id = uuid.uuid4().hex - self.msg_id = None - def send(self, exchange, routing_key='', confirm=True, - wait_for_reply=False, mandatory=True, persistent=False, - timeout=None, retrier=None): + def _prepare_message_to_send(self): msg = self.message.copy() msg['_unique_id'] = self.unique_id @@ -172,46 +175,158 @@ class PikaOutgoingMessage(object): for key, value in self.context.iteritems(): key = six.text_type(key) msg['_context_' + key] = value + return msg + @staticmethod + def _publish(pool, exchange, routing_key, body, properties, mandatory, + expiration_time): + timeout = (None if expiration_time is None else + expiration_time - time.time()) + if timeout is not None and timeout < 0: + raise exceptions.MessagingTimeout( + "Timeout for current operation was expired." + ) + try: + with pool.acquire(timeout=timeout) as conn: + if timeout is not None: + properties.expiration = str(int(timeout * 1000)) + conn.channel.publish( + exchange=exchange, + routing_key=routing_key, + body=body, + properties=properties, + mandatory=mandatory + ) + except pika_exceptions.NackError as e: + raise pika_drv_exc.MessageRejectedException( + "Can not send message: [body: {}], properties: {}] to " + "target [exchange: {}, routing_key: {}]. {}".format( + body, properties, exchange, routing_key, str(e) + ) + ) + except pika_exceptions.UnroutableError as e: + raise pika_drv_exc.RoutingException( + "Can not deliver message:[body:{}, properties: {}] to any" + "queue using target: [exchange:{}, " + "routing_key:{}]. {}".format( + body, properties, exchange, routing_key, str(e) + ) + ) + except pika_pool.Timeout as e: + raise exceptions.MessagingTimeout( + "Timeout for current operation was expired. {}".format(str(e)) + ) + except pika_pool.Connection.connectivity_errors as e: + if (isinstance(e, pika_exceptions.ChannelClosed) + and e.args and e.args[0] == 404): + raise pika_drv_exc.ExchangeNotFoundException( + "Attempt to send message to not existing exchange " + "detected, message: [body:{}, properties: {}], target: " + "[exchange:{}, routing_key:{}]. {}".format( + body, properties, exchange, routing_key, str(e) + ) + ) + + raise pika_drv_exc.ConnectionException( + "Connectivity problem detected during sending the message: " + "[body:{}, properties: {}] to target: [exchange:{}, " + "routing_key:{}]. {}".format( + body, properties, exchange, routing_key, str(e) + ) + ) + except socket.timeout: + raise pika_drv_exc.TimeoutConnectionException( + "Socket timeout exceeded." + ) + + def _do_send(self, exchange, routing_key, msg_dict, confirm=True, + mandatory=True, persistent=False, expiration_time=None, + retrier=None): properties = pika_spec.BasicProperties( content_encoding=self.content_encoding, content_type=self.content_type, delivery_mode=2 if persistent else 1 ) - expiration_time = ( - None if timeout is None else (timeout + time.time()) + pool = (self._pika_engine.connection_with_confirmation_pool + if confirm else self._pika_engine.connection_pool) + + body = jsonutils.dumps( + common.serialize_msg(msg_dict), + encoding=self.content_encoding ) - if wait_for_reply: - self.msg_id = uuid.uuid4().hex - msg['_msg_id'] = self.msg_id - LOG.debug('MSG_ID is %s', self.msg_id) + LOG.debug( + "Sending message:[body:{}; properties: {}] to target: " + "[exchange:{}; routing_key:{}]".format( + body, properties, exchange, routing_key + ) + ) - msg['_reply_q'] = self._pika_engine.get_reply_q(timeout) + publish = (self._publish if retrier is None else + retrier(self._publish)) + return publish(pool, exchange, routing_key, body, properties, + mandatory, expiration_time) + + def send(self, exchange, routing_key='', confirm=True, mandatory=True, + persistent=False, expiration_time=None, retrier=None): + msg_dict = self._prepare_message_to_send() + + return self._do_send(exchange, routing_key, msg_dict, confirm, + mandatory, persistent, expiration_time, retrier) + + +class RpcPikaOutgoingMessage(PikaOutgoingMessage): + def __init__(self, pika_engine, message, context, + content_type="application/json", content_encoding="utf-8"): + super(RpcPikaOutgoingMessage, self).__init__( + pika_engine, message, context, content_type, content_encoding + ) + self.msg_id = None + self.reply_q = None + + def send(self, target, reply_listener=None, expiration_time=None, + retrier=None): + + exchange = self._pika_engine.get_rpc_exchange_name( + target.exchange, target.topic, target.fanout, retrier is None + ) + + queue = "" if target.fanout else self._pika_engine.get_rpc_queue_name( + target.topic, target.server, retrier is None + ) + + msg_dict = self._prepare_message_to_send() + + if reply_listener: + msg_id = uuid.uuid4().hex + msg_dict["_msg_id"] = msg_id + LOG.debug('MSG_ID is %s', msg_id) + + msg_dict["_reply_q"] = reply_listener.get_reply_qname( + expiration_time - time.time() + ) future = futures.Future() - self._pika_engine.register_reply_waiter( - msg_id=self.msg_id, future=future, + reply_listener.register_reply_waiter( + msg_id=msg_id, future=future, expiration_time=expiration_time ) - self._pika_engine.publish( - exchange=exchange, routing_key=routing_key, - body=jsonutils.dumps( - common.serialize_msg(msg), - encoding=self.content_encoding - ), - properties=properties, - confirm=confirm, - mandatory=mandatory, - expiration_time=expiration_time, - retrier=retrier - ) + self._do_send( + exchange=exchange, routing_key=queue, msg_dict=msg_dict, + confirm=True, mandatory=True, persistent=False, + expiration_time=expiration_time, retrier=retrier + ) - if wait_for_reply: try: - return future.result(timeout) + return future.result(expiration_time - time.time()) except futures.TimeoutError: raise exceptions.MessagingTimeout() + else: + self._do_send( + exchange=exchange, routing_key=queue, msg_dict=msg_dict, + confirm=True, mandatory=True, persistent=False, + expiration_time=expiration_time, retrier=retrier + ) diff --git a/oslo_messaging/_drivers/pika_driver/pika_poller.py b/oslo_messaging/_drivers/pika_driver/pika_poller.py new file mode 100644 index 000000000..c7c152609 --- /dev/null +++ b/oslo_messaging/_drivers/pika_driver/pika_poller.py @@ -0,0 +1,293 @@ +# Copyright 2015 Mirantis, 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. + +import collections + +from oslo_log import log as logging + +import threading +import time + +from oslo_messaging._drivers.pika_driver import pika_message as pika_drv_msg +import pika_pool +import retrying + +LOG = logging.getLogger(__name__) + + +class PikaPoller(object): + def __init__(self, pika_engine, prefetch_count): + self._pika_engine = pika_engine + + self._connection = None + self._channel = None + self._lock = threading.Lock() + + self._prefetch_count = prefetch_count + + self._started = False + + self._queues_to_consume = None + + self._message_queue = collections.deque() + + def _reconnect(self): + self._connection = self._pika_engine.create_connection( + for_listening=True + ) + self._channel = self._connection.channel() + self._channel.basic_qos(prefetch_count=self._prefetch_count) + + if self._queues_to_consume is None: + self._declare_queue_binding() + + for queue, no_ack in self._queues_to_consume.iteritems(): + self._start_consuming(queue, no_ack) + + def _declare_queue_binding(self): + raise NotImplementedError( + "It is base class. Please declare exchanges and queues here" + ) + + def _start_consuming(self, queue, no_ack): + on_message_no_ack_callback = ( + self._on_message_no_ack_callback if no_ack + else self._on_message_with_ack_callback + ) + + try: + self._channel.basic_consume(on_message_no_ack_callback, queue, + no_ack=no_ack) + except Exception: + self._queues_to_consume = None + raise + + def _on_message_no_ack_callback(self, unused, method, properties, body): + self._message_queue.append( + (self._channel, method, properties, body, True) + ) + + def _on_message_with_ack_callback(self, unused, method, properties, body): + self._message_queue.append( + (self._channel, method, properties, body, False) + ) + + def _cleanup(self): + if self._channel: + try: + self._channel.close() + except Exception as ex: + if not pika_pool.Connection.is_connection_invalidated(ex): + LOG.exception("Unexpected error during closing channel") + self._channel = None + + if self._connection: + try: + self._connection.close() + except Exception as ex: + if not pika_pool.Connection.is_connection_invalidated(ex): + LOG.exception("Unexpected error during closing connection") + self._connection = None + + def poll(self, timeout=None): + start = time.time() + while not self._message_queue: + with self._lock: + if not self._started: + return None + + try: + if self._channel is None: + self._reconnect() + self._connection.process_data_events() + except Exception: + self._cleanup() + raise + if timeout and time.time() - start > timeout: + return None + + return self._message_queue.popleft() + + def start(self): + self._started = True + + def stop(self): + with self._lock: + if not self._started: + return + + self._started = False + self._cleanup() + + def reconnect(self): + with self._lock: + self._cleanup() + try: + self._reconnect() + except Exception: + self._cleanup() + raise + + def cleanup(self): + with self._lock: + self._cleanup() + + +class RpcServicePikaPoller(PikaPoller): + def __init__(self, pika_engine, target, prefetch_count): + self._target = target + + super(RpcServicePikaPoller, self).__init__( + pika_engine, prefetch_count=prefetch_count) + + def _declare_queue_binding(self): + queue_expiration = ( + self._pika_engine.conf.oslo_messaging_pika.rpc_queue_expiration + ) + + queues_to_consume = {} + + for no_ack in [True, False]: + exchange = self._pika_engine.get_rpc_exchange_name( + self._target.exchange, self._target.topic, False, no_ack + ) + fanout_exchange = self._pika_engine.get_rpc_exchange_name( + self._target.exchange, self._target.topic, True, no_ack + ) + queue = self._pika_engine.get_rpc_queue_name( + self._target.topic, None, no_ack + ) + server_queue = self._pika_engine.get_rpc_queue_name( + self._target.topic, self._target.server, no_ack + ) + + queues_to_consume[queue] = no_ack + queues_to_consume[server_queue] = no_ack + + self._pika_engine.declare_queue_binding_by_channel( + channel=self._channel, exchange=exchange, queue=queue, + routing_key=queue, exchange_type='direct', durable=False, + queue_expiration=queue_expiration, queue_auto_delete=False + ) + self._pika_engine.declare_queue_binding_by_channel( + channel=self._channel, exchange=exchange, queue=server_queue, + routing_key=server_queue, exchange_type='direct', + queue_expiration=queue_expiration, queue_auto_delete=False, + durable=False + ) + self._pika_engine.declare_queue_binding_by_channel( + channel=self._channel, exchange=fanout_exchange, durable=False, + queue=server_queue, routing_key="", exchange_type='fanout', + queue_expiration=queue_expiration, queue_auto_delete=False, + ) + self._queues_to_consume = queues_to_consume + + def poll(self, timeout=None): + msg = super(RpcServicePikaPoller, self).poll(timeout) + if msg is None: + return None + return pika_drv_msg.RpcPikaIncomingMessage( + self._pika_engine, *msg + ) + + +class RpcReplyPikaPoller(PikaPoller): + def __init__(self, pika_engine, exchange, queue, prefetch_count): + self._exchange = exchange + self._queue = queue + + super(RpcReplyPikaPoller, self).__init__( + pika_engine, prefetch_count + ) + + def _declare_queue_binding(self): + queue_expiration = ( + self._pika_engine.conf.oslo_messaging_pika.rpc_queue_expiration + ) + + self._pika_engine.declare_queue_binding_by_channel( + channel=self._channel, + exchange=self._exchange, queue=self._queue, + routing_key=self._queue, exchange_type='direct', + queue_expiration=queue_expiration, queue_auto_delete=False, + durable=False + ) + + self._queues_to_consume = {self._queue: False} + + def start(self, timeout=None): + super(RpcReplyPikaPoller, self).start() + + def on_exception(ex): + LOG.warn(str(ex)) + + return True + + retrier = retrying.retry( + stop_max_attempt_number=self._pika_engine.rpc_reply_retry_attempts, + stop_max_delay=None if timeout is None else timeout * 1000, + wait_fixed=self._pika_engine.rpc_reply_retry_delay * 1000, + retry_on_exception=on_exception, + ) + + retrier(self.reconnect)() + + def poll(self, timeout=None): + msg = super(RpcReplyPikaPoller, self).poll(timeout) + if msg is None: + return None + return pika_drv_msg.PikaIncomingMessage( + self._pika_engine, *msg + ) + + +class NotificationPikaPoller(PikaPoller): + def __init__(self, pika_engine, targets_and_priorities, + queue_name=None, prefetch_count=100): + self._targets_and_priorities = targets_and_priorities + self._queue_name = queue_name + + super(NotificationPikaPoller, self).__init__( + pika_engine, prefetch_count=prefetch_count + ) + + def _declare_queue_binding(self): + queues_to_consume = {} + for target, priority in self._targets_and_priorities: + routing_key = '%s.%s' % (target.topic, priority) + queue = self._queue_name or routing_key + self._pika_engine.declare_queue_binding_by_channel( + channel=self._channel, + exchange=( + target.exchange or + self._pika_engine.default_notification_exchange + ), + queue = queue, + routing_key=routing_key, + exchange_type='direct', + queue_expiration=None, + queue_auto_delete=False, + durable=self._pika_engine.notification_persistence, + ) + queues_to_consume[queue] = False + + self._queues_to_consume = queues_to_consume + + def poll(self, timeout=None): + msg = super(NotificationPikaPoller, self).poll(timeout) + if msg is None: + return None + return pika_drv_msg.PikaIncomingMessage( + self._pika_engine, *msg + ) From e24f4faf9681525817f33966c3d5602075febc7c Mon Sep 17 00:00:00 2001 From: Dmitriy Ukhlov Date: Thu, 19 Nov 2015 14:51:02 +0200 Subject: [PATCH 07/16] Fix delay before host reconnecting Change-Id: Ifd66b3fe44f5451ef4b5e03d06f214199474ca0b --- oslo_messaging/_drivers/impl_pika.py | 2 +- .../_drivers/pika_driver/pika_engine.py | 4 +- .../_drivers/pika_driver/pika_listener.py | 37 ++++++++----------- .../_drivers/pika_driver/pika_message.py | 5 ++- .../_drivers/pika_driver/pika_poller.py | 13 +++++-- 5 files changed, 31 insertions(+), 30 deletions(-) diff --git a/oslo_messaging/_drivers/impl_pika.py b/oslo_messaging/_drivers/impl_pika.py index b4b6d4288..843753648 100644 --- a/oslo_messaging/_drivers/impl_pika.py +++ b/oslo_messaging/_drivers/impl_pika.py @@ -47,7 +47,7 @@ pika_opts = [ cfg.FloatOpt('tcp_user_timeout', default=0.25, help="Set TCP_USER_TIMEOUT in seconds for connection's " "socket"), - cfg.FloatOpt('host_connection_reconnect_delay', default=5, + cfg.FloatOpt('host_connection_reconnect_delay', default=0.25, help="Set delay for reconnection to some host which has " "connection error") ] diff --git a/oslo_messaging/_drivers/pika_driver/pika_engine.py b/oslo_messaging/_drivers/pika_driver/pika_engine.py index 03ccccdf0..9d203bfce 100644 --- a/oslo_messaging/_drivers/pika_driver/pika_engine.py +++ b/oslo_messaging/_drivers/pika_driver/pika_engine.py @@ -121,7 +121,7 @@ class PikaEngine(object): "integer") self._tcp_user_timeout = self.conf.oslo_messaging_pika.tcp_user_timeout - self._host_connection_reconnect_delay = ( + self.host_connection_reconnect_delay = ( self.conf.oslo_messaging_pika.host_connection_reconnect_delay ) @@ -249,7 +249,7 @@ class PikaEngine(object): ] if (last_time != last_success_time and cur_time - last_time < - self._host_connection_reconnect_delay): + self.host_connection_reconnect_delay): raise pika_drv_exc.HostConnectionNotAllowedException( "Connection to host #{} is not allowed now because of " "previous failure".format(host_index) diff --git a/oslo_messaging/_drivers/pika_driver/pika_listener.py b/oslo_messaging/_drivers/pika_driver/pika_listener.py index 493adf1ea..88fc2586c 100644 --- a/oslo_messaging/_drivers/pika_driver/pika_listener.py +++ b/oslo_messaging/_drivers/pika_driver/pika_listener.py @@ -14,13 +14,14 @@ from oslo_log import log as logging +from oslo_messaging._drivers.pika_driver import pika_exceptions as pika_drv_exc from oslo_messaging._drivers.pika_driver import pika_poller as pika_drv_poller - import threading import time import uuid + LOG = logging.getLogger(__name__) @@ -32,7 +33,7 @@ class RpcReplyPikaListener(object): self._reply_queue = None self._reply_poller = None - self._reply_waiting_future_list = [] + self._reply_waiting_futures = {} self._reply_consumer_enabled = False self._reply_consumer_thread_run_flag = True @@ -83,27 +84,21 @@ class RpcReplyPikaListener(object): if message is None: continue message.acknowledge() - i = 0 - curtime = time.time() - while (i < len(self._reply_waiting_future_list) and - self._reply_consumer_thread_run_flag): - msg_id, future, expiration = ( - self._reply_waiting_future_list[i] - ) - if expiration and expiration < curtime: - del self._reply_waiting_future_list[i] - elif msg_id == message.msg_id: - del self._reply_waiting_future_list[i] - future.set_result(message) - else: - i += 1 + future = self._reply_waiting_futures.pop(message.msg_id, None) + if future is not None: + future.set_result(message) + except pika_drv_exc.EstablishConnectionException: + LOG.exception("Problem during establishing connection for " + "reply polling") + time.sleep(self._pika_engine.host_connection_reconnect_delay) except BaseException: - LOG.exception("Exception during reply polling") + LOG.exception("Unexpected exception during reply polling") - def register_reply_waiter(self, msg_id, future, expiration_time): - self._reply_waiting_future_list.append( - (msg_id, future, expiration_time) - ) + def register_reply_waiter(self, msg_id, future): + self._reply_waiting_futures[msg_id] = future + + def unregister_reply_waiter(self, msg_id): + self._reply_waiting_futures.pop(msg_id, None) def cleanup(self): with self._reply_consumer_lock: diff --git a/oslo_messaging/_drivers/pika_driver/pika_message.py b/oslo_messaging/_drivers/pika_driver/pika_message.py index f771b7cab..da87c7d01 100644 --- a/oslo_messaging/_drivers/pika_driver/pika_message.py +++ b/oslo_messaging/_drivers/pika_driver/pika_message.py @@ -310,8 +310,7 @@ class RpcPikaOutgoingMessage(PikaOutgoingMessage): future = futures.Future() reply_listener.register_reply_waiter( - msg_id=msg_id, future=future, - expiration_time=expiration_time + msg_id=msg_id, future=future ) self._do_send( @@ -324,6 +323,8 @@ class RpcPikaOutgoingMessage(PikaOutgoingMessage): return future.result(expiration_time - time.time()) except futures.TimeoutError: raise exceptions.MessagingTimeout() + finally: + reply_listener.unregister_reply_waiter(self.msg_id) else: self._do_send( exchange=exchange, routing_key=queue, msg_dict=msg_dict, diff --git a/oslo_messaging/_drivers/pika_driver/pika_poller.py b/oslo_messaging/_drivers/pika_driver/pika_poller.py index c7c152609..d34a9c11a 100644 --- a/oslo_messaging/_drivers/pika_driver/pika_poller.py +++ b/oslo_messaging/_drivers/pika_driver/pika_poller.py @@ -101,7 +101,8 @@ class PikaPoller(object): self._connection = None def poll(self, timeout=None): - start = time.time() + expiration_time = time.time() + timeout if timeout else None + while not self._message_queue: with self._lock: if not self._started: @@ -110,12 +111,16 @@ class PikaPoller(object): try: if self._channel is None: self._reconnect() - self._connection.process_data_events() + self._connection.process_data_events( + time_limit=timeout + ) except Exception: self._cleanup() raise - if timeout and time.time() - start > timeout: - return None + if timeout is not None: + timeout = expiration_time - time.time() + if timeout <= 0: + return None return self._message_queue.popleft() From 8caa4bef8412a03bae45aaab675b4c98e2ef7fe3 Mon Sep 17 00:00:00 2001 From: Dmitriy Ukhlov Date: Mon, 23 Nov 2015 14:19:44 +0200 Subject: [PATCH 08/16] Removes additional select module patching Change-Id: I9ad8a3ffec2c41ef7b88a522e0d643d29be86af0 --- oslo_messaging/_drivers/impl_pika.py | 29 ++----------- .../_drivers/pika_driver/pika_engine.py | 41 +++++++++++++++---- 2 files changed, 36 insertions(+), 34 deletions(-) diff --git a/oslo_messaging/_drivers/impl_pika.py b/oslo_messaging/_drivers/impl_pika.py index 843753648..bdab583e2 100644 --- a/oslo_messaging/_drivers/impl_pika.py +++ b/oslo_messaging/_drivers/impl_pika.py @@ -12,10 +12,6 @@ # License for the specific language governing permissions and limitations # under the License. -import retrying -import sys -import time - from oslo_config import cfg from oslo_log import log as logging @@ -29,6 +25,9 @@ from oslo_messaging._drivers.pika_driver import pika_poller as pika_drv_poller from oslo_messaging import exceptions +import retrying +import time + LOG = logging.getLogger(__name__) pika_opts = [ @@ -133,31 +132,9 @@ rpc_opts = [ ] -def _is_eventlet_monkey_patched(): - if 'eventlet.patcher' not in sys.modules: - return False - import eventlet.patcher - return eventlet.patcher.is_monkey_patched('thread') - - class PikaDriver(object): def __init__(self, conf, url, default_exchange=None, allowed_remote_exmods=None): - if 'eventlet.patcher' in sys.modules: - import eventlet.patcher - if eventlet.patcher.is_monkey_patched('select'): - import select - - try: - del select.poll - except AttributeError: - pass - - try: - del select.epoll - except AttributeError: - pass - opt_group = cfg.OptGroup(name='oslo_messaging_pika', title='Pika driver options') conf.register_group(opt_group) diff --git a/oslo_messaging/_drivers/pika_driver/pika_engine.py b/oslo_messaging/_drivers/pika_driver/pika_engine.py index 9d203bfce..745c2da1c 100644 --- a/oslo_messaging/_drivers/pika_driver/pika_engine.py +++ b/oslo_messaging/_drivers/pika_driver/pika_engine.py @@ -11,8 +11,6 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -import socket - from oslo_log import log as logging from oslo_messaging import exceptions @@ -20,18 +18,41 @@ from oslo_messaging import exceptions from oslo_messaging._drivers.pika_driver import pika_exceptions as pika_drv_exc import pika -from pika import adapters as pika_adapters +from pika.adapters import select_connection from pika import credentials as pika_credentials import pika_pool +import socket +import sys + import threading import time LOG = logging.getLogger(__name__) -class PooledConnectionWithConfirmations(pika_pool.Connection): +def _is_eventlet_monkey_patched(module): + if 'eventlet.patcher' not in sys.modules: + return False + import eventlet.patcher + return eventlet.patcher.is_monkey_patched(module) + + +def _create__select_poller_connection_impl( + parameters, on_open_callback, on_open_error_callback, + on_close_callback, stop_ioloop_on_close): + return select_connection.SelectConnection( + parameters=parameters, + on_open_callback=on_open_callback, + on_open_error_callback=on_open_error_callback, + on_close_callback=on_close_callback, + stop_ioloop_on_close=stop_ioloop_on_close, + custom_ioloop=select_connection.SelectPoller() + ) + + +class _PooledConnectionWithConfirmations(pika_pool.Connection): @property def channel(self): if self.fairy.channel is None: @@ -49,6 +70,8 @@ class PikaEngine(object): def __init__(self, conf, url, default_exchange=None): self.conf = conf + self._force_select_poller_use = _is_eventlet_monkey_patched('select') + # processing rpc options self.default_rpc_exchange = ( @@ -177,7 +200,7 @@ class PikaEngine(object): ) self.connection_with_confirmation_pool.Connection = ( - PooledConnectionWithConfirmations + _PooledConnectionWithConfirmations ) def _next_connection_num(self): @@ -258,14 +281,16 @@ class PikaEngine(object): try: base_host_params = self._connection_host_param_list[host_index] - connection = pika_adapters.BlockingConnection( - pika.ConnectionParameters( + connection = pika.BlockingConnection( + parameters=pika.ConnectionParameters( heartbeat_interval=( self.conf.oslo_messaging_pika.heartbeat_interval if for_listening else None ), **base_host_params - ) + ), + _impl_class=(_create__select_poller_connection_impl + if self._force_select_poller_use else None) ) self._set_tcp_user_timeout(connection._impl.socket) From 46ac91e63ed39ec575b1b4a9f47becc87eb80511 Mon Sep 17 00:00:00 2001 From: Alexey Lebedeff Date: Tue, 24 Nov 2015 14:57:24 +0300 Subject: [PATCH 09/16] Provide missing parts of error messages Change-Id: I8ccb8a4979ec78b7f87d32d455265eeb57ed442f --- .../_drivers/pika_driver/pika_message.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/oslo_messaging/_drivers/pika_driver/pika_message.py b/oslo_messaging/_drivers/pika_driver/pika_message.py index da87c7d01..b08288c71 100644 --- a/oslo_messaging/_drivers/pika_driver/pika_message.py +++ b/oslo_messaging/_drivers/pika_driver/pika_message.py @@ -54,8 +54,12 @@ class PikaIncomingMessage(object): ) if self.content_type != "application/json": - raise NotImplementedError("Content-type['{}'] is not valid, " - "'application/json' only is supported.") + raise NotImplementedError( + "Content-type['{}'] is not valid, " + "'application/json' only is supported.".format( + self.content_type + ) + ) message_dict = common.deserialize_msg( jsonutils.loads(body, encoding=self.content_encoding) @@ -159,8 +163,12 @@ class PikaOutgoingMessage(object): self.content_encoding = content_encoding if self.content_type != "application/json": - raise NotImplementedError("Content-type['{}'] is not valid, " - "'application/json' only is supported.") + raise NotImplementedError( + "Content-type['{}'] is not valid, " + "'application/json' only is supported.".format( + self.content_type + ) + ) self.message = message self.context = context From a30fcdf585653fb0b18d39e55695be5efd82a5c8 Mon Sep 17 00:00:00 2001 From: dukhlov Date: Fri, 27 Nov 2015 14:42:56 -0500 Subject: [PATCH 10/16] Adds comments and small fixes Fixes in this patch: 1) Set time_timit=0.25 for process_data_events in poll It is needed to release lock time to time and give a chance another method to be executed 2) move declare_queue_bindind method from pika_engine to impl_pika module because it is not common and used only there 3) small optimization - now unregister_reply_waiter is called in worst case only - if we have not got reply by timeout expiration Change-Id: I18b6cdec9c1f746086a5a1ae4ecd8d8ecdaa9468 --- oslo_messaging/_drivers/impl_pika.py | 40 ++++-- .../_drivers/pika_driver/pika_engine.py | 130 ++++++++++++------ .../_drivers/pika_driver/pika_exceptions.py | 34 +++-- .../_drivers/pika_driver/pika_listener.py | 80 +++++++---- .../_drivers/pika_driver/pika_message.py | 10 +- .../_drivers/pika_driver/pika_poller.py | 16 ++- 6 files changed, 211 insertions(+), 99 deletions(-) diff --git a/oslo_messaging/_drivers/impl_pika.py b/oslo_messaging/_drivers/impl_pika.py index bdab583e2..4ccf346d4 100644 --- a/oslo_messaging/_drivers/impl_pika.py +++ b/oslo_messaging/_drivers/impl_pika.py @@ -25,6 +25,8 @@ from oslo_messaging._drivers.pika_driver import pika_poller as pika_drv_poller from oslo_messaging import exceptions +import pika_pool + import retrying import time @@ -198,6 +200,31 @@ class PikaDriver(object): return reply.message['result'] + def _declare_notification_queue_binding(self, target, timeout=None): + if timeout is not None and timeout < 0: + raise exceptions.MessagingTimeout( + "Timeout for current operation was expired." + ) + try: + with self._pika_engine.connection_pool.acquire( + timeout=timeout) as conn: + self._pika_engine.declare_queue_binding_by_channel( + conn.channel, + exchange=( + target.exchange or + self._pika_engine.default_notification_exchange + ), + queue=target.topic, + routing_key=target.topic, + exchange_type='direct', + queue_expiration=None, + durable=self._pika_engine.notification_persistence, + ) + except pika_pool.Timeout as e: + raise exceptions.MessagingTimeout( + "Timeout for current operation was expired. {}.".format(str(e)) + ) + def send_notification(self, target, ctxt, message, version, retry=None): if retry is None: retry = self._pika_engine.default_notification_retry_attempts @@ -207,18 +234,7 @@ class PikaDriver(object): pika_drv_exc.RoutingException)): LOG.warn(str(ex)) try: - self._pika_engine.declare_queue_binding( - exchange=( - target.exchange or - self._pika_engine.default_notification_exchange - ), - queue=target.topic, - routing_key=target.topic, - exchange_type='direct', - queue_expiration=None, - queue_auto_delete=False, - durable=self._pika_engine.notification_persistence, - ) + self._declare_notification_queue_binding(target) except pika_drv_exc.ConnectionException as e: LOG.warn(str(e)) return True diff --git a/oslo_messaging/_drivers/pika_driver/pika_engine.py b/oslo_messaging/_drivers/pika_driver/pika_engine.py index 745c2da1c..f8060cc13 100644 --- a/oslo_messaging/_drivers/pika_driver/pika_engine.py +++ b/oslo_messaging/_drivers/pika_driver/pika_engine.py @@ -13,8 +13,6 @@ # under the License. from oslo_log import log as logging -from oslo_messaging import exceptions - from oslo_messaging._drivers.pika_driver import pika_exceptions as pika_drv_exc import pika @@ -33,6 +31,12 @@ LOG = logging.getLogger(__name__) def _is_eventlet_monkey_patched(module): + """Determines safely is eventlet patching for module enabled or not + + :param module: String, module name + :return Bool, True if module is pathed, False otherwise + """ + if 'eventlet.patcher' not in sys.modules: return False import eventlet.patcher @@ -42,6 +46,14 @@ def _is_eventlet_monkey_patched(module): def _create__select_poller_connection_impl( parameters, on_open_callback, on_open_error_callback, on_close_callback, stop_ioloop_on_close): + """Used for disabling autochoise of poller ('select', 'poll', 'epool', etc) + inside default 'SelectConnection.__init__(...)' logic. It is necessary to + force 'select' poller usage if eventlet is monkeypatched because eventlet + patches only 'select' system call + + Method signature is copied form 'SelectConnection.__init__(...)', because + it is used as replacement of 'SelectConnection' class to create instances + """ return select_connection.SelectConnection( parameters=parameters, on_open_callback=on_open_callback, @@ -53,6 +65,10 @@ def _create__select_poller_connection_impl( class _PooledConnectionWithConfirmations(pika_pool.Connection): + """Derived from 'pika_pool.Connection' and extends its logic - adds + 'confirm_delivery' call after channel creation to enable delivery + confirmation for channel + """ @property def channel(self): if self.fairy.channel is None: @@ -62,9 +78,17 @@ class _PooledConnectionWithConfirmations(pika_pool.Connection): class PikaEngine(object): + """Used for shared functionality between other pika driver modules, like + connection factory, connection pools, processing and holding configuration, + etc. + """ + + # constants for creating connection statistics HOST_CONNECTION_LAST_TRY_TIME = "last_try_time" HOST_CONNECTION_LAST_SUCCESS_TRY_TIME = "last_success_try_time" + # constant for setting tcp_user_timeout socket option + # (it should be defined in 'select' module of standard library in future) TCP_USER_TIMEOUT = 18 def __init__(self, conf, url, default_exchange=None): @@ -73,7 +97,6 @@ class PikaEngine(object): self._force_select_poller_use = _is_eventlet_monkey_patched('select') # processing rpc options - self.default_rpc_exchange = ( conf.oslo_messaging_pika.default_rpc_exchange if conf.oslo_messaging_pika.default_rpc_exchange else @@ -106,6 +129,10 @@ class PikaEngine(object): raise ValueError("rpc_reply_retry_delay should be non-negative " "integer") + self.rpc_queue_expiration = ( + self.conf.oslo_messaging_pika.rpc_queue_expiration + ) + # processing notification options self.default_notification_exchange = ( conf.oslo_messaging_pika.default_notification_exchange if @@ -147,6 +174,9 @@ class PikaEngine(object): self.host_connection_reconnect_delay = ( self.conf.oslo_messaging_pika.host_connection_reconnect_delay ) + self._heartbeat_interval = ( + self.conf.oslo_messaging_pika.heartbeat_interval + ) # initializing connection parameters for configured RabbitMQ hosts common_pika_params = { @@ -204,6 +234,11 @@ class PikaEngine(object): ) def _next_connection_num(self): + """Used for creating connections to different RabbitMQ nodes in + round robin order + + :return: next host number to create connection to + """ with self._connection_lock: cur_num = self._next_connection_host_num self._next_connection_host_num += 1 @@ -215,7 +250,7 @@ class PikaEngine(object): def create_connection(self, for_listening=False): """Create and return connection to any available host. - :return: cerated connection + :return: created connection :raise: ConnectionException if all hosts are not reachable """ host_count = len(self._connection_host_param_list) @@ -257,7 +292,9 @@ class PikaEngine(object): def create_host_connection(self, host_index, for_listening=False): """Create new connection to host #host_index - + :param host_index: Integer, number of host for connection establishing + :param for_listening: Boolean, creates connection for listening + (enable heartbeats) if True :return: New connection """ @@ -270,6 +307,10 @@ class PikaEngine(object): last_time = self._connection_host_status_list[host_index][ self.HOST_CONNECTION_LAST_TRY_TIME ] + + # raise HostConnectionNotAllowedException if we tried to establish + # connection in last 'host_connection_reconnect_delay' and got + # failure if (last_time != last_success_time and cur_time - last_time < self.host_connection_reconnect_delay): @@ -284,7 +325,7 @@ class PikaEngine(object): connection = pika.BlockingConnection( parameters=pika.ConnectionParameters( heartbeat_interval=( - self.conf.oslo_messaging_pika.heartbeat_interval + self._heartbeat_interval if for_listening else None ), **base_host_params @@ -308,52 +349,53 @@ class PikaEngine(object): @staticmethod def declare_queue_binding_by_channel(channel, exchange, queue, routing_key, exchange_type, queue_expiration, - queue_auto_delete, durable): - channel.exchange_declare( - exchange, exchange_type, auto_delete=True, durable=durable - ) - arguments = {} + durable): + """Declare exchange, queue and bind them using already created + channel, if they don't exist - if queue_expiration > 0: - arguments['x-expires'] = queue_expiration * 1000 - - channel.queue_declare( - queue, auto_delete=queue_auto_delete, durable=durable, - arguments=arguments - ) - - channel.queue_bind(queue, exchange, routing_key) - - def declare_queue_binding(self, exchange, queue, routing_key, - exchange_type, queue_expiration, - queue_auto_delete, durable, - timeout=None): - if timeout is not None and timeout < 0: - raise exceptions.MessagingTimeout( - "Timeout for current operation was expired." - ) + :param channel: Channel for communication with RabbitMQ + :param exchange: String, RabbitMQ exchange name + :param queue: Sting, RabbitMQ queue name + :param routing_key: Sting, RabbitMQ routing key for queue binding + :param exchange_type: String ('direct', 'topic' or 'fanout') + exchange type for exchange to be declared + :param queue_expiration: Integer, time in seconds which queue will + remain existing in RabbitMQ when there no consumers connected + :param durable: Boolean, creates durable exchange and queue if true + """ try: - with self.connection_pool.acquire(timeout=timeout) as conn: - self.declare_queue_binding_by_channel( - conn.channel, exchange, queue, routing_key, exchange_type, - queue_expiration, queue_auto_delete, durable - ) - except pika_pool.Timeout as e: - raise exceptions.MessagingTimeout( - "Timeout for current operation was expired. {}.".format(str(e)) + channel.exchange_declare( + exchange, exchange_type, auto_delete=True, durable=durable ) + arguments = {} + + if queue_expiration > 0: + arguments['x-expires'] = queue_expiration * 1000 + + channel.queue_declare(queue, durable=durable, arguments=arguments) + + channel.queue_bind(queue, exchange, routing_key) except pika_pool.Connection.connectivity_errors as e: raise pika_drv_exc.ConnectionException( "Connectivity problem detected during declaring queue " "binding: exchange:{}, queue: {}, routing_key: {}, " - "exchange_type: {}, queue_expiration: {}, queue_auto_delete: " - "{}, durable: {}. {}".format( + "exchange_type: {}, queue_expiration: {}, " + "durable: {}. {}".format( exchange, queue, routing_key, exchange_type, - queue_expiration, queue_auto_delete, durable, str(e) + queue_expiration, durable, str(e) ) ) def get_rpc_exchange_name(self, exchange, topic, fanout, no_ack): + """Returns RabbitMQ exchange name for given rpc request + + :param exchange: String, oslo.messaging target's exchange + :param topic: String, oslo.messaging target's topic + :param fanout: Boolean, oslo.messaging target's fanout mode + :param no_ack: Boolean, use message delivery with acknowledges or not + + :return: String, RabbitMQ exchange name + """ exchange = (exchange or self.default_rpc_exchange) if fanout: @@ -364,6 +406,14 @@ class PikaEngine(object): @staticmethod def get_rpc_queue_name(topic, server, no_ack): + """Returns RabbitMQ queue name for given rpc request + + :param topic: String, oslo.messaging target's topic + :param server: String, oslo.messaging target's server + :param no_ack: Boolean, use message delivery with acknowledges or not + + :return: String, RabbitMQ exchange name + """ queue_parts = ["no_ack" if no_ack else "with_ack", topic] if server is not None: queue_parts.append(server) diff --git a/oslo_messaging/_drivers/pika_driver/pika_exceptions.py b/oslo_messaging/_drivers/pika_driver/pika_exceptions.py index 62e370fa3..89e191560 100644 --- a/oslo_messaging/_drivers/pika_driver/pika_exceptions.py +++ b/oslo_messaging/_drivers/pika_driver/pika_exceptions.py @@ -16,28 +16,46 @@ from oslo_messaging import exceptions class ExchangeNotFoundException(exceptions.MessageDeliveryFailure): + """Is raised if specified exchange is not found in RabbitMQ.""" pass class MessageRejectedException(exceptions.MessageDeliveryFailure): + """Is raised if message which you are trying to send was nacked by RabbitMQ + it may happen if RabbitMQ is not able to process message + """ pass class RoutingException(exceptions.MessageDeliveryFailure): + """Is raised if message can not be delivered to any queue. Usually it means + that any queue is not binded to given exchange with given routing key. + Raised if 'mandatory' flag specified only + """ pass class ConnectionException(exceptions.MessagingException): - pass - - -class HostConnectionNotAllowedException(ConnectionException): - pass - - -class EstablishConnectionException(ConnectionException): + """Is raised if some operation can not be performed due to connectivity + problem + """ pass class TimeoutConnectionException(ConnectionException): + """Is raised if socket timeout was expired during network interaction""" + pass + + +class EstablishConnectionException(ConnectionException): + """Is raised if we have some problem during establishing connection + procedure + """ + pass + + +class HostConnectionNotAllowedException(EstablishConnectionException): + """Is raised in case of try to establish connection to temporary + not allowed host (because of reconnection policy for example) + """ pass diff --git a/oslo_messaging/_drivers/pika_driver/pika_listener.py b/oslo_messaging/_drivers/pika_driver/pika_listener.py index 88fc2586c..cfbb5b8de 100644 --- a/oslo_messaging/_drivers/pika_driver/pika_listener.py +++ b/oslo_messaging/_drivers/pika_driver/pika_listener.py @@ -26,6 +26,10 @@ LOG = logging.getLogger(__name__) class RpcReplyPikaListener(object): + """Provide functionality for listening RPC replies. Create and handle + reply poller and coroutine for performing polling job + """ + def __init__(self, pika_engine): self._pika_engine = pika_engine @@ -35,25 +39,34 @@ class RpcReplyPikaListener(object): self._reply_poller = None self._reply_waiting_futures = {} - self._reply_consumer_enabled = False - self._reply_consumer_thread_run_flag = True - self._reply_consumer_lock = threading.Lock() - self._puller_thread = None + self._reply_consumer_initialized = False + self._reply_consumer_initialization_lock = threading.Lock() + self._poller_thread = None def get_reply_qname(self, expiration_time=None): - if self._reply_consumer_enabled: + """As result return reply queue name, shared for whole process, + but before this check is RPC listener initialized or not and perform + initialization if needed + + :param expiration_time: Float, expiration time in seconds + (like time.time()), + :return: String, queue name which hould be used for reply sending + """ + if self._reply_consumer_initialized: return self._reply_queue - with self._reply_consumer_lock: - if self._reply_consumer_enabled: + with self._reply_consumer_initialization_lock: + if self._reply_consumer_initialized: return self._reply_queue + # generate reply queue name if needed if self._reply_queue is None: self._reply_queue = "reply.{}.{}.{}".format( self._pika_engine.conf.project, self._pika_engine.conf.prog, uuid.uuid4().hex ) + # initialize reply poller if needed if self._reply_poller is None: self._reply_poller = pika_drv_poller.RpcReplyPikaPoller( pika_engine=self._pika_engine, @@ -66,21 +79,25 @@ class RpcReplyPikaListener(object): self._reply_poller.start(timeout=expiration_time - time.time()) - if self._puller_thread is None: - self._puller_thread = threading.Thread(target=self._poller) - self._puller_thread.daemon = True + # start reply poller job thread if needed + if self._poller_thread is None: + self._poller_thread = threading.Thread(target=self._poller) + self._poller_thread.daemon = True - if not self._puller_thread.is_alive(): - self._puller_thread.start() + if not self._poller_thread.is_alive(): + self._poller_thread.start() - self._reply_consumer_enabled = True + self._reply_consumer_initialized = True return self._reply_queue def _poller(self): - while self._reply_consumer_thread_run_flag: + """Reply polling job. Poll replies in infinite loop and notify + registered features + """ + while self._reply_poller: try: - message = self._reply_poller.poll(timeout=1) + message = self._reply_poller.poll() if message is None: continue message.acknowledge() @@ -95,24 +112,31 @@ class RpcReplyPikaListener(object): LOG.exception("Unexpected exception during reply polling") def register_reply_waiter(self, msg_id, future): + """Register reply waiter. Should be called before message sending to + the server + :param msg_id: String, message_id of expected reply + :param future: Future, container for expected reply to be returned over + """ self._reply_waiting_futures[msg_id] = future def unregister_reply_waiter(self, msg_id): + """Unregister reply waiter. Should be called if client has not got + reply and doesn't want to continue waiting (if timeout_expired for + example) + :param msg_id: + """ self._reply_waiting_futures.pop(msg_id, None) def cleanup(self): - with self._reply_consumer_lock: - self._reply_consumer_enabled = False + """Stop replies consuming and cleanup resources""" + if self._reply_poller: + self._reply_poller.stop() + self._reply_poller.cleanup() + self._reply_poller = None - if self._puller_thread: - if self._puller_thread.is_alive(): - self._reply_consumer_thread_run_flag = False - self._puller_thread.join() - self._puller_thread = None + if self._poller_thread: + if self._poller_thread.is_alive(): + self._poller_thread.join() + self._poller_thread = None - if self._reply_poller: - self._reply_poller.stop() - self._reply_poller.cleanup() - self._reply_poller = None - - self._reply_queue = None + self._reply_queue = None diff --git a/oslo_messaging/_drivers/pika_driver/pika_message.py b/oslo_messaging/_drivers/pika_driver/pika_message.py index da87c7d01..2b4bbbc26 100644 --- a/oslo_messaging/_drivers/pika_driver/pika_message.py +++ b/oslo_messaging/_drivers/pika_driver/pika_message.py @@ -125,7 +125,7 @@ class RpcPikaIncomingMessage(PikaIncomingMessage): ), retry_on_exception=on_exception, wait_fixed=self._pika_engine.rpc_reply_retry_delay * 1000, - ) + ) if self._pika_engine.rpc_reply_retry_attempts else None try: reply_outgoing_message.send( @@ -321,10 +321,12 @@ class RpcPikaOutgoingMessage(PikaOutgoingMessage): try: return future.result(expiration_time - time.time()) - except futures.TimeoutError: - raise exceptions.MessagingTimeout() - finally: + except BaseException as e: reply_listener.unregister_reply_waiter(self.msg_id) + if isinstance(e, futures.TimeoutError): + e = exceptions.MessagingTimeout() + raise e + else: self._do_send( exchange=exchange, routing_key=queue, msg_dict=msg_dict, diff --git a/oslo_messaging/_drivers/pika_driver/pika_poller.py b/oslo_messaging/_drivers/pika_driver/pika_poller.py index d34a9c11a..a6dd7e093 100644 --- a/oslo_messaging/_drivers/pika_driver/pika_poller.py +++ b/oslo_messaging/_drivers/pika_driver/pika_poller.py @@ -111,8 +111,12 @@ class PikaPoller(object): try: if self._channel is None: self._reconnect() + # we need some time_limit here, not too small to avoid a + # lot of not needed iterations but not too large to release + # lock time to time and give a chance to perform another + # method waiting this lock self._connection.process_data_events( - time_limit=timeout + time_limit=0.25 ) except Exception: self._cleanup() @@ -183,18 +187,17 @@ class RpcServicePikaPoller(PikaPoller): self._pika_engine.declare_queue_binding_by_channel( channel=self._channel, exchange=exchange, queue=queue, routing_key=queue, exchange_type='direct', durable=False, - queue_expiration=queue_expiration, queue_auto_delete=False + queue_expiration=queue_expiration ) self._pika_engine.declare_queue_binding_by_channel( channel=self._channel, exchange=exchange, queue=server_queue, routing_key=server_queue, exchange_type='direct', - queue_expiration=queue_expiration, queue_auto_delete=False, - durable=False + queue_expiration=queue_expiration, durable=False ) self._pika_engine.declare_queue_binding_by_channel( channel=self._channel, exchange=fanout_exchange, durable=False, queue=server_queue, routing_key="", exchange_type='fanout', - queue_expiration=queue_expiration, queue_auto_delete=False, + queue_expiration=queue_expiration ) self._queues_to_consume = queues_to_consume @@ -225,7 +228,7 @@ class RpcReplyPikaPoller(PikaPoller): channel=self._channel, exchange=self._exchange, queue=self._queue, routing_key=self._queue, exchange_type='direct', - queue_expiration=queue_expiration, queue_auto_delete=False, + queue_expiration=queue_expiration, durable=False ) @@ -282,7 +285,6 @@ class NotificationPikaPoller(PikaPoller): routing_key=routing_key, exchange_type='direct', queue_expiration=None, - queue_auto_delete=False, durable=self._pika_engine.notification_persistence, ) queues_to_consume[queue] = False From bbf0efa29e5b5aede406f1e1f52f2e34941e9e11 Mon Sep 17 00:00:00 2001 From: dukhlov Date: Thu, 3 Dec 2015 05:51:00 -0500 Subject: [PATCH 11/16] Preparations for configurable serialization This patch moves message envelope logic to driver level and separates serialization logic from envelop logic Change-Id: I33c52193357fe298d82b1eb36c9b95edf7e500a4 --- oslo_messaging/_drivers/impl_pika.py | 13 +- .../_drivers/pika_driver/pika_engine.py | 10 +- .../_drivers/pika_driver/pika_exceptions.py | 7 + .../_drivers/pika_driver/pika_message.py | 137 +++++++++++++++--- .../_drivers/pika_driver/pika_poller.py | 2 +- 5 files changed, 134 insertions(+), 35 deletions(-) diff --git a/oslo_messaging/_drivers/impl_pika.py b/oslo_messaging/_drivers/impl_pika.py index 4ccf346d4..a017e5690 100644 --- a/oslo_messaging/_drivers/impl_pika.py +++ b/oslo_messaging/_drivers/impl_pika.py @@ -15,8 +15,6 @@ from oslo_config import cfg from oslo_log import log as logging -from oslo_messaging._drivers import common - from oslo_messaging._drivers.pika_driver import pika_engine as pika_drv_engine from oslo_messaging._drivers.pika_driver import pika_exceptions as pika_drv_exc from oslo_messaging._drivers.pika_driver import pika_listener as pika_drv_lstnr @@ -149,7 +147,7 @@ class PikaDriver(object): self._allowed_remote_exmods = allowed_remote_exmods self._pika_engine = pika_drv_engine.PikaEngine( - conf, url, default_exchange + conf, url, default_exchange, allowed_remote_exmods ) self._reply_listener = pika_drv_lstnr.RpcReplyPikaListener( self._pika_engine @@ -192,13 +190,10 @@ class PikaDriver(object): ) if reply is not None: - if reply.message['failure']: - ex = common.deserialize_remote_exception( - reply.message['failure'], self._allowed_remote_exmods - ) - raise ex + if reply.failure is not None: + raise reply.failure - return reply.message['result'] + return reply.result def _declare_notification_queue_binding(self, target, timeout=None): if timeout is not None and timeout < 0: diff --git a/oslo_messaging/_drivers/pika_driver/pika_engine.py b/oslo_messaging/_drivers/pika_driver/pika_engine.py index f8060cc13..88e90c0b0 100644 --- a/oslo_messaging/_drivers/pika_driver/pika_engine.py +++ b/oslo_messaging/_drivers/pika_driver/pika_engine.py @@ -21,6 +21,7 @@ from pika import credentials as pika_credentials import pika_pool +import six import socket import sys @@ -29,6 +30,8 @@ import time LOG = logging.getLogger(__name__) +_EXCEPTIONS_MODULE = 'exceptions' if six.PY2 else 'builtins' + def _is_eventlet_monkey_patched(module): """Determines safely is eventlet patching for module enabled or not @@ -91,7 +94,8 @@ class PikaEngine(object): # (it should be defined in 'select' module of standard library in future) TCP_USER_TIMEOUT = 18 - def __init__(self, conf, url, default_exchange=None): + def __init__(self, conf, url, default_exchange=None, + allowed_remote_exmods=None): self.conf = conf self._force_select_poller_use = _is_eventlet_monkey_patched('select') @@ -108,6 +112,10 @@ class PikaEngine(object): default_exchange ) + self.allowed_remote_exmods = [_EXCEPTIONS_MODULE] + if allowed_remote_exmods: + self.allowed_remote_exmods.extend(allowed_remote_exmods) + self.rpc_listener_prefetch_count = ( conf.oslo_messaging_pika.rpc_listener_prefetch_count ) diff --git a/oslo_messaging/_drivers/pika_driver/pika_exceptions.py b/oslo_messaging/_drivers/pika_driver/pika_exceptions.py index 89e191560..c32d7e401 100644 --- a/oslo_messaging/_drivers/pika_driver/pika_exceptions.py +++ b/oslo_messaging/_drivers/pika_driver/pika_exceptions.py @@ -59,3 +59,10 @@ class HostConnectionNotAllowedException(EstablishConnectionException): not allowed host (because of reconnection policy for example) """ pass + + +class UnsupportedDriverVersion(exceptions.MessagingException): + """Is raised when message is received but was sent by different, + not supported driver version + """ + pass diff --git a/oslo_messaging/_drivers/pika_driver/pika_message.py b/oslo_messaging/_drivers/pika_driver/pika_message.py index 60f073c72..2fe4acaed 100644 --- a/oslo_messaging/_drivers/pika_driver/pika_message.py +++ b/oslo_messaging/_drivers/pika_driver/pika_message.py @@ -11,38 +11,66 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. + + +import socket +import time +import traceback +import uuid + from concurrent import futures - from oslo_log import log as logging - -from oslo_messaging._drivers import common -from oslo_messaging import exceptions - -from oslo_messaging._drivers.pika_driver import pika_exceptions as pika_drv_exc - from oslo_serialization import jsonutils - +from oslo_utils import importutils from pika import exceptions as pika_exceptions from pika import spec as pika_spec import pika_pool - import retrying import six -import socket -import time -import uuid + + +import oslo_messaging +from oslo_messaging._drivers.pika_driver import pika_exceptions as pika_drv_exc +from oslo_messaging import _utils as utils +from oslo_messaging import exceptions + LOG = logging.getLogger(__name__) +_VERSION_HEADER = "version" +_VERSION = "1.0" + + +class RemoteExceptionMixin(object): + def __init__(self, module, clazz, message, trace): + self.module = module + self.clazz = clazz + self.message = message + self.trace = trace + + self._str_msgs = message + "\n" + "\n".join(trace) + + def __str__(self): + return self._str_msgs + class PikaIncomingMessage(object): def __init__(self, pika_engine, channel, method, properties, body, no_ack): + headers = getattr(properties, "headers", {}) + version = headers.get(_VERSION_HEADER, None) + if not utils.version_is_compatible(version, _VERSION): + raise pika_drv_exc.UnsupportedDriverVersion( + "Message's version: {} is not compatible with driver version: " + "{}".format(version, _VERSION)) + self._pika_engine = pika_engine self._no_ack = no_ack self._channel = channel self.delivery_tag = method.delivery_tag + self.version = version + self.content_type = getattr(properties, "content_type", "application/json") self.content_encoding = getattr(properties, "content_encoding", @@ -61,9 +89,7 @@ class PikaIncomingMessage(object): ) ) - message_dict = common.deserialize_msg( - jsonutils.loads(body, encoding=self.content_encoding) - ) + message_dict = jsonutils.loads(body, encoding=self.content_encoding) context_dict = {} @@ -101,15 +127,37 @@ class RpcPikaIncomingMessage(PikaIncomingMessage): if not (self.msg_id and self.reply_q): return - if failure: - failure = common.serialize_remote_exception(failure, log_failure) - msg = { - 'result': reply, - 'failure': failure, '_msg_id': self.msg_id, } + if failure is not None: + if isinstance(failure, RemoteExceptionMixin): + failure_data = { + 'class': failure.clazz, + 'module': failure.module, + 'message': failure.message, + 'tb': failure.trace + } + else: + tb = traceback.format_exception(*failure) + failure = failure[1] + + cls_name = six.text_type(failure.__class__.__name__) + mod_name = six.text_type(failure.__class__.__module__) + + failure_data = { + 'class': cls_name, + 'module': mod_name, + 'message': six.text_type(failure), + 'tb': tb + } + + msg['_failure'] = failure_data + + if reply is not None: + msg['_result'] = reply + reply_outgoing_message = PikaOutgoingMessage( self._pika_engine, msg, self.ctxt, content_type=self.content_type, content_encoding=self.content_encoding @@ -154,6 +202,49 @@ class RpcPikaIncomingMessage(PikaIncomingMessage): ) +class RpcReplyPikaIncomingMessage(PikaIncomingMessage): + def __init__(self, pika_engine, channel, method, properties, body, no_ack): + self.result = None + self.failure = None + + super(RpcReplyPikaIncomingMessage, self).__init__( + pika_engine, channel, method, properties, body, no_ack + ) + + if self.failure is not None: + trace = self.failure.get('tb', []) + message = self.failure.get('message', "") + class_name = self.failure.get('class') + module_name = self.failure.get('module') + + res_exc = None + + if module_name in pika_engine.allowed_remote_exmods: + try: + module = importutils.import_module(module_name) + klass = getattr(module, class_name) + + ex_type = type( + klass.__name__, + (RemoteExceptionMixin, klass), + {} + ) + + res_exc = ex_type(module_name, class_name, message, trace) + except ImportError as e: + LOG.warn( + "Can not deserialize remote exception [module:{}, " + "class:{}]. {}".format(module_name, class_name, str(e)) + ) + + # if we have not processed failure yet, use RemoteError class + if res_exc is None: + res_exc = oslo_messaging.RemoteError( + class_name, message, trace + ) + self.failure = res_exc + + class PikaOutgoingMessage(object): def __init__(self, pika_engine, message, context, content_type="application/json", content_encoding="utf-8"): @@ -253,16 +344,14 @@ class PikaOutgoingMessage(object): properties = pika_spec.BasicProperties( content_encoding=self.content_encoding, content_type=self.content_type, + headers={_VERSION_HEADER: _VERSION}, delivery_mode=2 if persistent else 1 ) pool = (self._pika_engine.connection_with_confirmation_pool if confirm else self._pika_engine.connection_pool) - body = jsonutils.dumps( - common.serialize_msg(msg_dict), - encoding=self.content_encoding - ) + body = jsonutils.dumps(msg_dict, encoding=self.content_encoding) LOG.debug( "Sending message:[body:{}; properties: {}] to target: " diff --git a/oslo_messaging/_drivers/pika_driver/pika_poller.py b/oslo_messaging/_drivers/pika_driver/pika_poller.py index a6dd7e093..8d1b1f58b 100644 --- a/oslo_messaging/_drivers/pika_driver/pika_poller.py +++ b/oslo_messaging/_drivers/pika_driver/pika_poller.py @@ -255,7 +255,7 @@ class RpcReplyPikaPoller(PikaPoller): msg = super(RpcReplyPikaPoller, self).poll(timeout) if msg is None: return None - return pika_drv_msg.PikaIncomingMessage( + return pika_drv_msg.RpcReplyPikaIncomingMessage( self._pika_engine, *msg ) From 438a808c9150acec074499a70f6f2772c91dee8a Mon Sep 17 00:00:00 2001 From: Dmitriy Ukhlov Date: Fri, 4 Dec 2015 18:45:46 +0200 Subject: [PATCH 12/16] Adds comment, updates pika-pool version Change-Id: I3d9702bfdde89279258dbf0af027fa2a4a044edd --- .../_drivers/pika_driver/pika_message.py | 160 ++++++++++++++++++ requirements.txt | 2 +- 2 files changed, 161 insertions(+), 1 deletion(-) diff --git a/oslo_messaging/_drivers/pika_driver/pika_message.py b/oslo_messaging/_drivers/pika_driver/pika_message.py index 2fe4acaed..05f6fcd17 100644 --- a/oslo_messaging/_drivers/pika_driver/pika_message.py +++ b/oslo_messaging/_drivers/pika_driver/pika_message.py @@ -42,7 +42,20 @@ _VERSION = "1.0" class RemoteExceptionMixin(object): + """Used for constructing dynamic exception type during deserialization of + remote exception. It defines unified '__init__' method signature and + exception message format + """ def __init__(self, module, clazz, message, trace): + """Store serialized data + :param module: String, module name for importing original exception + class of serialized remote exception + :param clazz: String, original class name of serialized remote + exception + :param message: String, original message of serialized remote + exception + :param trace: String, original trace of serialized remote exception + """ self.module = module self.clazz = clazz self.message = message @@ -55,8 +68,23 @@ class RemoteExceptionMixin(object): class PikaIncomingMessage(object): + """Driver friendly adapter for received message. Extract message + information from RabbitMQ message and provide access to it + """ def __init__(self, pika_engine, channel, method, properties, body, no_ack): + """Parse RabbitMQ message + + :param pika_engine: PikaEngine, shared object with configuration and + shared driver functionality + :param channel: Channel, RabbitMQ channel which was used for + this message delivery + :param method: Method, RabbitMQ message method + :param properties: Properties, RabbitMQ message properties + :param body: Bytes, RabbitMQ message body + :param no_ack: Boolean, defines should this message be acked by + consumer or not + """ headers = getattr(properties, "headers", {}) version = headers.get(_VERSION_HEADER, None) if not utils.version_is_compatible(version, _VERSION): @@ -105,17 +133,43 @@ class PikaIncomingMessage(object): self.ctxt = context_dict def acknowledge(self): + """Ack the message. Should be called by message processing logic when + it considered as consumed (means that we don't need redelivery of this + message anymore) + """ if not self._no_ack: self._channel.basic_ack(delivery_tag=self.delivery_tag) def requeue(self): + """Rollback the message. Should be called by message processing logic + when it can not process the message right now and should be redelivered + later if it is possible + """ if not self._no_ack: return self._channel.basic_nack(delivery_tag=self.delivery_tag, requeue=True) class RpcPikaIncomingMessage(PikaIncomingMessage): + """PikaIncomingMessage implementation for RPC messages. It expects + extra RPC related fields in message body (msg_id and reply_q). Also 'reply' + method added to allow consumer to send RPC reply back to the RPC client + """ + def __init__(self, pika_engine, channel, method, properties, body, no_ack): + """Defines default values of msg_id and reply_q fields and just call + super.__init__ method + + :param pika_engine: PikaEngine, shared object with configuration and + shared driver functionality + :param channel: Channel, RabbitMQ channel which was used for + this message delivery + :param method: Method, RabbitMQ message method + :param properties: Properties, RabbitMQ message properties + :param body: Bytes, RabbitMQ message body + :param no_ack: Boolean, defines should this message be acked by + consumer or not + """ self.msg_id = None self.reply_q = None @@ -124,6 +178,13 @@ class RpcPikaIncomingMessage(PikaIncomingMessage): ) def reply(self, reply=None, failure=None, log_failure=True): + """Send back reply to the RPC client + :param reply - Dictionary, reply. In case of exception should be None + :param failure - Exception, exception, raised during processing RPC + request. Should be None if RPC request was successfully processed + :param log_failure, Boolean, not used in this implementation. + It present here to be compatible with driver API + """ if not (self.msg_id and self.reply_q): return @@ -203,7 +264,24 @@ class RpcPikaIncomingMessage(PikaIncomingMessage): class RpcReplyPikaIncomingMessage(PikaIncomingMessage): + """PikaIncomingMessage implementation for RPC reply messages. It expects + extra RPC reply related fields in message body (result and failure). + """ def __init__(self, pika_engine, channel, method, properties, body, no_ack): + """Defines default values of result and failure fields, call + super.__init__ method and then construct Exception object if failure is + not None + + :param pika_engine: PikaEngine, shared object with configuration and + shared driver functionality + :param channel: Channel, RabbitMQ channel which was used for + this message delivery + :param method: Method, RabbitMQ message method + :param properties: Properties, RabbitMQ message properties + :param body: Bytes, RabbitMQ message body + :param no_ack: Boolean, defines should this message be acked by + consumer or not + """ self.result = None self.failure = None @@ -246,8 +324,23 @@ class RpcReplyPikaIncomingMessage(PikaIncomingMessage): class PikaOutgoingMessage(object): + """Driver friendly adapter for sending message. Construct RabbitMQ message + and send it + """ + def __init__(self, pika_engine, message, context, content_type="application/json", content_encoding="utf-8"): + """Parse RabbitMQ message + + :param pika_engine: PikaEngine, shared object with configuration and + shared driver functionality + :param message: Dictionary, user's message fields + :param context: Dictionary, request context's fields + :param content_type: String, content-type header, defines serialization + mechanism + :param content_encoding: String, defines encoding for text data + """ + self._pika_engine = pika_engine self.content_type = content_type @@ -267,6 +360,17 @@ class PikaOutgoingMessage(object): self.unique_id = uuid.uuid4().hex def _prepare_message_to_send(self): + """Combine user's message fields an system fields (_unique_id, + context's data etc) + + :param pika_engine: PikaEngine, shared object with configuration and + shared driver functionality + :param message: Dictionary, user's message fields + :param context: Dictionary, request context's fields + :param content_type: String, content-type header, defines serialization + mechanism + :param content_encoding: String, defines encoding for text data + """ msg = self.message.copy() msg['_unique_id'] = self.unique_id @@ -279,6 +383,20 @@ class PikaOutgoingMessage(object): @staticmethod def _publish(pool, exchange, routing_key, body, properties, mandatory, expiration_time): + """Execute pika publish method using connection from connection pool + Also this message catches all pika related exceptions and raise + oslo.messaging specific exceptions + + :param pool: Pool, pika connection pool for connection choosing + :param exchange: String, RabbitMQ exchange name for message sending + :param routing_key: String, RabbitMQ routing key for message routing + :param body: Bytes, RabbitMQ message payload + :param properties: Properties, RabbitMQ message properties + :param mandatory: Boolean, RabbitMQ publish mandatory flag (raise + exception if it is not possible to deliver message to any queue) + :param expiration_time: Float, expiration time in seconds + (like time.time()) + """ timeout = (None if expiration_time is None else expiration_time - time.time()) if timeout is not None and timeout < 0: @@ -341,6 +459,21 @@ class PikaOutgoingMessage(object): def _do_send(self, exchange, routing_key, msg_dict, confirm=True, mandatory=True, persistent=False, expiration_time=None, retrier=None): + """Send prepared message with configured retrying + + :param exchange: String, RabbitMQ exchange name for message sending + :param routing_key: String, RabbitMQ routing key for message routing + :param msg_dict: Dictionary, message payload + :param confirm: Boolean, enable publisher confirmation if True + :param mandatory: Boolean, RabbitMQ publish mandatory flag (raise + exception if it is not possible to deliver message to any queue) + :param persistent: Boolean, send persistent message if True, works only + for routing into durable queues + :param expiration_time: Float, expiration time in seconds + (like time.time()) + :param retrier: retrying.Retrier, configured retrier object for sending + message, if None no retrying is performed + """ properties = pika_spec.BasicProperties( content_encoding=self.content_encoding, content_type=self.content_type, @@ -368,6 +501,20 @@ class PikaOutgoingMessage(object): def send(self, exchange, routing_key='', confirm=True, mandatory=True, persistent=False, expiration_time=None, retrier=None): + """Send message with configured retrying + + :param exchange: String, RabbitMQ exchange name for message sending + :param routing_key: String, RabbitMQ routing key for message routing + :param confirm: Boolean, enable publisher confirmation if True + :param mandatory: Boolean, RabbitMQ publish mandatory flag (raise + exception if it is not possible to deliver message to any queue) + :param persistent: Boolean, send persistent message if True, works only + for routing into durable queues + :param expiration_time: Float, expiration time in seconds + (like time.time()) + :param retrier: retrying.Retrier, configured retrier object for sending + message, if None no retrying is performed + """ msg_dict = self._prepare_message_to_send() return self._do_send(exchange, routing_key, msg_dict, confirm, @@ -375,6 +522,9 @@ class PikaOutgoingMessage(object): class RpcPikaOutgoingMessage(PikaOutgoingMessage): + """PikaOutgoingMessage implementation for RPC messages. It adds + possibility to wait and receive RPC reply + """ def __init__(self, pika_engine, message, context, content_type="application/json", content_encoding="utf-8"): super(RpcPikaOutgoingMessage, self).__init__( @@ -385,6 +535,16 @@ class RpcPikaOutgoingMessage(PikaOutgoingMessage): def send(self, target, reply_listener=None, expiration_time=None, retrier=None): + """Send RPC message with configured retrying + + :param target: Target, oslo.messaging target which defines RPC service + :param reply_listener: RpcReplyPikaListener, listener for waiting + reply. If None - return immediately without reply waiting + :param expiration_time: Float, expiration time in seconds + (like time.time()) + :param retrier: retrying.Retrier, configured retrier object for sending + message, if None no retrying is performed + """ exchange = self._pika_engine.get_rpc_exchange_name( target.exchange, target.topic, target.fanout, retrier is None diff --git a/requirements.txt b/requirements.txt index 681d55ec9..fa33c2437 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,7 +34,7 @@ PyYAML>=3.1.0 amqp>=1.4.0 kombu>=3.0.7 pika>=0.10.0 -pika-pool>=0.1.2 +pika-pool>=0.1.3 # middleware oslo.middleware>=2.8.0 # Apache-2.0 From bee303cf6f302ede14dd13c26d063cbe607e270c Mon Sep 17 00:00:00 2001 From: Dmitriy Ukhlov Date: Wed, 9 Dec 2015 14:09:13 +0200 Subject: [PATCH 13/16] Adds comment for pika_pooler.py Also this patch: 1) fixed import's order 2) removes PikaDriverCompatibleWithOldRabbit (tests, show that after retry implementation all works fine) Change-Id: Ib34f6db569cadb5c27d8865f13ba32ef9a6c73e9 --- oslo_messaging/_drivers/impl_pika.py | 37 +---- .../_drivers/pika_driver/pika_engine.py | 16 +- .../_drivers/pika_driver/pika_listener.py | 17 +- .../_drivers/pika_driver/pika_message.py | 5 +- .../_drivers/pika_driver/pika_poller.py | 154 +++++++++++++++++- setup.cfg | 2 +- 6 files changed, 170 insertions(+), 61 deletions(-) diff --git a/oslo_messaging/_drivers/impl_pika.py b/oslo_messaging/_drivers/impl_pika.py index a017e5690..eaa4a7685 100644 --- a/oslo_messaging/_drivers/impl_pika.py +++ b/oslo_messaging/_drivers/impl_pika.py @@ -12,8 +12,13 @@ # License for the specific language governing permissions and limitations # under the License. +import time + from oslo_config import cfg from oslo_log import log as logging +from oslo_messaging import exceptions +import pika_pool +import retrying from oslo_messaging._drivers.pika_driver import pika_engine as pika_drv_engine from oslo_messaging._drivers.pika_driver import pika_exceptions as pika_drv_exc @@ -21,13 +26,6 @@ from oslo_messaging._drivers.pika_driver import pika_listener as pika_drv_lstnr from oslo_messaging._drivers.pika_driver import pika_message as pika_drv_msg from oslo_messaging._drivers.pika_driver import pika_poller as pika_drv_poller -from oslo_messaging import exceptions - -import pika_pool - -import retrying -import time - LOG = logging.getLogger(__name__) pika_opts = [ @@ -144,7 +142,6 @@ class PikaDriver(object): conf.register_opts(notification_opts, group=opt_group) self.conf = conf - self._allowed_remote_exmods = allowed_remote_exmods self._pika_engine = pika_drv_engine.PikaEngine( conf, url, default_exchange, allowed_remote_exmods @@ -277,27 +274,3 @@ class PikaDriver(object): def cleanup(self): self._reply_listener.cleanup() - - -class PikaDriverCompatibleWithRabbitDriver(PikaDriver): - """Old RabbitMQ driver creates exchange before sending message. - In this case if no rpc service listen this exchange message will be sent - to /dev/null but client will know anything about it. That is strange. - But for now we need to keep original behaviour - """ - def send(self, target, ctxt, message, wait_for_reply=None, timeout=None, - retry=None): - try: - return super(PikaDriverCompatibleWithRabbitDriver, self).send( - target=target, - ctxt=ctxt, - message=message, - wait_for_reply=wait_for_reply, - timeout=timeout, - retry=retry - ) - except exceptions.MessageDeliveryFailure: - if wait_for_reply: - raise exceptions.MessagingTimeout() - else: - return None diff --git a/oslo_messaging/_drivers/pika_driver/pika_engine.py b/oslo_messaging/_drivers/pika_driver/pika_engine.py index 88e90c0b0..06dfcdbf1 100644 --- a/oslo_messaging/_drivers/pika_driver/pika_engine.py +++ b/oslo_messaging/_drivers/pika_driver/pika_engine.py @@ -11,22 +11,20 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. + +import socket +import sys +import threading +import time + from oslo_log import log as logging - -from oslo_messaging._drivers.pika_driver import pika_exceptions as pika_drv_exc - import pika from pika.adapters import select_connection from pika import credentials as pika_credentials - import pika_pool - import six -import socket -import sys -import threading -import time +from oslo_messaging._drivers.pika_driver import pika_exceptions as pika_drv_exc LOG = logging.getLogger(__name__) diff --git a/oslo_messaging/_drivers/pika_driver/pika_listener.py b/oslo_messaging/_drivers/pika_driver/pika_listener.py index cfbb5b8de..8eff1feb7 100644 --- a/oslo_messaging/_drivers/pika_driver/pika_listener.py +++ b/oslo_messaging/_drivers/pika_driver/pika_listener.py @@ -12,15 +12,15 @@ # License for the specific language governing permissions and limitations # under the License. -from oslo_log import log as logging - -from oslo_messaging._drivers.pika_driver import pika_exceptions as pika_drv_exc -from oslo_messaging._drivers.pika_driver import pika_poller as pika_drv_poller - import threading import time import uuid +from concurrent import futures +from oslo_log import log as logging + +from oslo_messaging._drivers.pika_driver import pika_exceptions as pika_drv_exc +from oslo_messaging._drivers.pika_driver import pika_poller as pika_drv_poller LOG = logging.getLogger(__name__) @@ -111,13 +111,16 @@ class RpcReplyPikaListener(object): except BaseException: LOG.exception("Unexpected exception during reply polling") - def register_reply_waiter(self, msg_id, future): + def register_reply_waiter(self, msg_id): """Register reply waiter. Should be called before message sending to the server :param msg_id: String, message_id of expected reply - :param future: Future, container for expected reply to be returned over + :return future: Future, container for expected reply to be returned + over """ + future = futures.Future() self._reply_waiting_futures[msg_id] = future + return future def unregister_reply_waiter(self, msg_id): """Unregister reply waiter. Should be called if client has not got diff --git a/oslo_messaging/_drivers/pika_driver/pika_message.py b/oslo_messaging/_drivers/pika_driver/pika_message.py index 05f6fcd17..9bf9febdb 100644 --- a/oslo_messaging/_drivers/pika_driver/pika_message.py +++ b/oslo_messaging/_drivers/pika_driver/pika_message.py @@ -564,11 +564,8 @@ class RpcPikaOutgoingMessage(PikaOutgoingMessage): msg_dict["_reply_q"] = reply_listener.get_reply_qname( expiration_time - time.time() ) - future = futures.Future() - reply_listener.register_reply_waiter( - msg_id=msg_id, future=future - ) + future = reply_listener.register_reply_waiter(msg_id=msg_id) self._do_send( exchange=exchange, routing_key=queue, msg_dict=msg_dict, diff --git a/oslo_messaging/_drivers/pika_driver/pika_poller.py b/oslo_messaging/_drivers/pika_driver/pika_poller.py index 8d1b1f58b..185c8d02a 100644 --- a/oslo_messaging/_drivers/pika_driver/pika_poller.py +++ b/oslo_messaging/_drivers/pika_driver/pika_poller.py @@ -13,21 +13,32 @@ # under the License. import collections - -from oslo_log import log as logging - import threading import time -from oslo_messaging._drivers.pika_driver import pika_message as pika_drv_msg +from oslo_log import log as logging import pika_pool import retrying +from oslo_messaging._drivers.pika_driver import pika_message as pika_drv_msg + LOG = logging.getLogger(__name__) class PikaPoller(object): + """Provides user friendly functionality for RabbitMQ message consuming, + handles low level connectivity problems and restore connection if some + connectivity related problem detected + """ + def __init__(self, pika_engine, prefetch_count): + """Initialize required fields + + :param pika_engine: PikaEngine, shared object with configuration and + shared driver functionality + :param prefetch_count: Integer, maximum count of unacknowledged + messages which RabbitMQ broker sends to this consumer + """ self._pika_engine = pika_engine self._connection = None @@ -43,6 +54,9 @@ class PikaPoller(object): self._message_queue = collections.deque() def _reconnect(self): + """Performs reconnection to the broker. It is unsafe method for + internal use only + """ self._connection = self._pika_engine.create_connection( for_listening=True ) @@ -50,17 +64,31 @@ class PikaPoller(object): self._channel.basic_qos(prefetch_count=self._prefetch_count) if self._queues_to_consume is None: - self._declare_queue_binding() + self._queues_to_consume = self._declare_queue_binding() for queue, no_ack in self._queues_to_consume.iteritems(): self._start_consuming(queue, no_ack) def _declare_queue_binding(self): + """Is called by recovering connection logic if target RabbitMQ + exchange and (or) queue do not exist. Should be overridden in child + classes + + :return Dictionary, declared_queue_name -> no_ack_mode + """ raise NotImplementedError( "It is base class. Please declare exchanges and queues here" ) def _start_consuming(self, queue, no_ack): + """Is called by recovering connection logic for starting consumption + of the RabbitMQ queue + + :param queue: String, RabbitMQ queue name for consuming + :param no_ack: Boolean, Choose consuming acknowledgement mode. If True, + acknowledges are not needed. RabbitMQ considers message consumed + after sending it to consumer immediately + """ on_message_no_ack_callback = ( self._on_message_no_ack_callback if no_ack else self._on_message_with_ack_callback @@ -74,16 +102,25 @@ class PikaPoller(object): raise def _on_message_no_ack_callback(self, unused, method, properties, body): + """Is called by Pika when message was received from queue listened with + no_ack=True mode + """ self._message_queue.append( (self._channel, method, properties, body, True) ) def _on_message_with_ack_callback(self, unused, method, properties, body): + """Is called by Pika when message was received from queue listened with + no_ack=False mode + """ self._message_queue.append( (self._channel, method, properties, body, False) ) def _cleanup(self): + """Cleanup allocated resources (channel, connection, etc). It is unsafe + method for internal use only + """ if self._channel: try: self._channel.close() @@ -101,6 +138,13 @@ class PikaPoller(object): self._connection = None def poll(self, timeout=None): + """Main method of this class - consumes message from RabbitMQ + + :param: timeout: float, seconds, timeout for waiting new incoming + message, None means wait forever + :return: tuple, RabbitMQ message related data + (channel, method, properties, body, no_ack) + """ expiration_time = time.time() + timeout if timeout else None while not self._message_queue: @@ -129,9 +173,16 @@ class PikaPoller(object): return self._message_queue.popleft() def start(self): + """Starts poller. Should be called before polling to allow message + consuming + """ self._started = True def stop(self): + """Stops poller. Should be called when polling is not needed anymore to + stop new message consuming. After that it is necessary to poll already + prefetched messages + """ with self._lock: if not self._started: return @@ -140,6 +191,7 @@ class PikaPoller(object): self._cleanup() def reconnect(self): + """Safe version of _reconnect. Performs reconnection to the broker.""" with self._lock: self._cleanup() try: @@ -149,18 +201,39 @@ class PikaPoller(object): raise def cleanup(self): + """Safe version of _cleanup. Cleans up allocated resources (channel, + connection, etc). + """ with self._lock: self._cleanup() class RpcServicePikaPoller(PikaPoller): + """PikaPoller implementation for polling RPC messages. Overrides base + functionality according to RPC specific + """ def __init__(self, pika_engine, target, prefetch_count): + """Adds target parameter for declaring RPC specific exchanges and + queues + + :param pika_engine: PikaEngine, shared object with configuration and + shared driver functionality + :param target: Target, oslo.messaging Target object which defines RPC + endpoint + :param prefetch_count: Integer, maximum count of unacknowledged + messages which RabbitMQ broker sends to this consumer + """ self._target = target super(RpcServicePikaPoller, self).__init__( pika_engine, prefetch_count=prefetch_count) def _declare_queue_binding(self): + """Overrides base method and perform declaration of RabbitMQ exchanges + and queues which correspond to oslo.messaging RPC target + + :return Dictionary, declared_queue_name -> no_ack_mode + """ queue_expiration = ( self._pika_engine.conf.oslo_messaging_pika.rpc_queue_expiration ) @@ -199,9 +272,16 @@ class RpcServicePikaPoller(PikaPoller): queue=server_queue, routing_key="", exchange_type='fanout', queue_expiration=queue_expiration ) - self._queues_to_consume = queues_to_consume + return queues_to_consume def poll(self, timeout=None): + """Overrides base method and wrap RabbitMQ message into + RpcPikaIncomingMessage + + :param: timeout: float, seconds, timeout for waiting new incoming + message, None means wait forever + :return: RpcPikaIncomingMessage, consumed RPC message + """ msg = super(RpcServicePikaPoller, self).poll(timeout) if msg is None: return None @@ -211,7 +291,20 @@ class RpcServicePikaPoller(PikaPoller): class RpcReplyPikaPoller(PikaPoller): + """PikaPoller implementation for polling RPC reply messages. Overrides + base functionality according to RPC reply specific + """ def __init__(self, pika_engine, exchange, queue, prefetch_count): + """Adds exchange and queue parameter for declaring exchange and queue + used for RPC reply delivery + + :param pika_engine: PikaEngine, shared object with configuration and + shared driver functionality + :param exchange: String, exchange name used for RPC reply delivery + :param queue: String, queue name used for RPC reply delivery + :param prefetch_count: Integer, maximum count of unacknowledged + messages which RabbitMQ broker sends to this consumer + """ self._exchange = exchange self._queue = queue @@ -220,6 +313,11 @@ class RpcReplyPikaPoller(PikaPoller): ) def _declare_queue_binding(self): + """Overrides base method and perform declaration of RabbitMQ exchange + and queue used for RPC reply delivery + + :return Dictionary, declared_queue_name -> no_ack_mode + """ queue_expiration = ( self._pika_engine.conf.oslo_messaging_pika.rpc_queue_expiration ) @@ -232,9 +330,15 @@ class RpcReplyPikaPoller(PikaPoller): durable=False ) - self._queues_to_consume = {self._queue: False} + return {self._queue: False} def start(self, timeout=None): + """Overrides default behaviour of start method. Base start method + does not create connection to RabbitMQ during start method (uses + lazy connecting during first poll method call). This class should be + connected after start call to ensure that exchange and queue for reply + delivery are created before RPC request sending + """ super(RpcReplyPikaPoller, self).start() def on_exception(ex): @@ -252,6 +356,13 @@ class RpcReplyPikaPoller(PikaPoller): retrier(self.reconnect)() def poll(self, timeout=None): + """Overrides base method and wrap RabbitMQ message into + RpcReplyPikaIncomingMessage + + :param: timeout: float, seconds, timeout for waiting new incoming + message, None means wait forever + :return: RpcReplyPikaIncomingMessage, consumed RPC reply message + """ msg = super(RpcReplyPikaPoller, self).poll(timeout) if msg is None: return None @@ -261,8 +372,23 @@ class RpcReplyPikaPoller(PikaPoller): class NotificationPikaPoller(PikaPoller): + """PikaPoller implementation for polling Notification messages. Overrides + base functionality according to Notification specific + """ def __init__(self, pika_engine, targets_and_priorities, queue_name=None, prefetch_count=100): + """Adds exchange and queue parameter for declaring exchange and queue + used for RPC reply delivery + + :param pika_engine: PikaEngine, shared object with configuration and + shared driver functionality + :param targets_and_priorities: list of (target, priority), defines + default queue names for corresponding notification types + :param queue: String, alternative queue name used for this poller + instead of default queue name + :param prefetch_count: Integer, maximum count of unacknowledged + messages which RabbitMQ broker sends to this consumer + """ self._targets_and_priorities = targets_and_priorities self._queue_name = queue_name @@ -271,6 +397,11 @@ class NotificationPikaPoller(PikaPoller): ) def _declare_queue_binding(self): + """Overrides base method and perform declaration of RabbitMQ exchanges + and queues used for notification delivery + + :return Dictionary, declared_queue_name -> no_ack_mode + """ queues_to_consume = {} for target, priority in self._targets_and_priorities: routing_key = '%s.%s' % (target.topic, priority) @@ -289,9 +420,16 @@ class NotificationPikaPoller(PikaPoller): ) queues_to_consume[queue] = False - self._queues_to_consume = queues_to_consume + return queues_to_consume def poll(self, timeout=None): + """Overrides base method and wrap RabbitMQ message into + PikaIncomingMessage + + :param: timeout: float, seconds, timeout for waiting new incoming + message, None means wait forever + :return: PikaIncomingMessage, consumed Notification message + """ msg = super(NotificationPikaPoller, self).poll(timeout) if msg is None: return None diff --git a/setup.cfg b/setup.cfg index 1524e0467..d5b4b08bc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -35,7 +35,7 @@ oslo.messaging.drivers = # This is just for internal testing fake = oslo_messaging._drivers.impl_fake:FakeDriver - pika = oslo_messaging._drivers.impl_pika:PikaDriverCompatibleWithRabbitDriver + pika = oslo_messaging._drivers.impl_pika:PikaDriver oslo.messaging.executors = aioeventlet = oslo_messaging._executors.impl_aioeventlet:AsyncioEventletExecutor From 3976a2ff81408b7f86e898eb0a87634a3f9ed2c0 Mon Sep 17 00:00:00 2001 From: Dmitriy Ukhlov Date: Mon, 14 Dec 2015 18:49:50 +0200 Subject: [PATCH 14/16] Fixes conflicts after merging master Change-Id: I0d75c19e3002a3aad2dd35bbaea203fa9ba0c0ea --- .../_drivers/pika_driver/pika_listener.py | 30 ++++-- .../_drivers/pika_driver/pika_poller.py | 94 +++++++------------ 2 files changed, 52 insertions(+), 72 deletions(-) diff --git a/oslo_messaging/_drivers/pika_driver/pika_listener.py b/oslo_messaging/_drivers/pika_driver/pika_listener.py index 8eff1feb7..2c33168e5 100644 --- a/oslo_messaging/_drivers/pika_driver/pika_listener.py +++ b/oslo_messaging/_drivers/pika_driver/pika_listener.py @@ -97,17 +97,27 @@ class RpcReplyPikaListener(object): """ while self._reply_poller: try: - message = self._reply_poller.poll() - if message is None: + try: + messages = self._reply_poller.poll() + except pika_drv_exc.EstablishConnectionException: + LOG.exception("Problem during establishing connection for " + "reply polling") + time.sleep( + self._pika_engine.host_connection_reconnect_delay + ) continue - message.acknowledge() - future = self._reply_waiting_futures.pop(message.msg_id, None) - if future is not None: - future.set_result(message) - except pika_drv_exc.EstablishConnectionException: - LOG.exception("Problem during establishing connection for " - "reply polling") - time.sleep(self._pika_engine.host_connection_reconnect_delay) + + for message in messages: + try: + message.acknowledge() + future = self._reply_waiting_futures.pop( + message.msg_id, None + ) + if future is not None: + future.set_result(message) + except Exception: + LOG.exception("Unexpected exception during processing" + "reply message") except BaseException: LOG.exception("Unexpected exception during reply polling") diff --git a/oslo_messaging/_drivers/pika_driver/pika_poller.py b/oslo_messaging/_drivers/pika_driver/pika_poller.py index 185c8d02a..1390ced75 100644 --- a/oslo_messaging/_drivers/pika_driver/pika_poller.py +++ b/oslo_messaging/_drivers/pika_driver/pika_poller.py @@ -12,7 +12,6 @@ # License for the specific language governing permissions and limitations # under the License. -import collections import threading import time @@ -31,27 +30,30 @@ class PikaPoller(object): connectivity related problem detected """ - def __init__(self, pika_engine, prefetch_count): + def __init__(self, pika_engine, prefetch_count, + incoming_message_class=pika_drv_msg.PikaIncomingMessage): """Initialize required fields :param pika_engine: PikaEngine, shared object with configuration and shared driver functionality :param prefetch_count: Integer, maximum count of unacknowledged messages which RabbitMQ broker sends to this consumer + :param incoming_message_class: PikaIncomingMessage, wrapper for + consumed RabbitMQ message """ self._pika_engine = pika_engine + self._prefetch_count = prefetch_count + self._incoming_message_class = incoming_message_class self._connection = None self._channel = None self._lock = threading.Lock() - self._prefetch_count = prefetch_count - self._started = False self._queues_to_consume = None - self._message_queue = collections.deque() + self._message_queue = [] def _reconnect(self): """Performs reconnection to the broker. It is unsafe method for @@ -106,7 +108,10 @@ class PikaPoller(object): no_ack=True mode """ self._message_queue.append( - (self._channel, method, properties, body, True) + self._incoming_message_class( + self._pika_engine, self._channel, method, properties, body, + True + ) ) def _on_message_with_ack_callback(self, unused, method, properties, body): @@ -114,7 +119,10 @@ class PikaPoller(object): no_ack=False mode """ self._message_queue.append( - (self._channel, method, properties, body, False) + self._incoming_message_class( + self._pika_engine, self._channel, method, properties, body, + False + ) ) def _cleanup(self): @@ -137,17 +145,19 @@ class PikaPoller(object): LOG.exception("Unexpected error during closing connection") self._connection = None - def poll(self, timeout=None): + def poll(self, timeout=None, prefetch_size=1): """Main method of this class - consumes message from RabbitMQ :param: timeout: float, seconds, timeout for waiting new incoming message, None means wait forever - :return: tuple, RabbitMQ message related data - (channel, method, properties, body, no_ack) + :param: prefetch_size: Integer, count of messages which we are want to + poll. It blocks until prefetch_size messages are consumed or until + timeout gets expired + :return: list of PikaIncomingMessage, RabbitMQ messages """ expiration_time = time.time() + timeout if timeout else None - while not self._message_queue: + while len(self._message_queue) < prefetch_size: with self._lock: if not self._started: return None @@ -162,15 +172,17 @@ class PikaPoller(object): self._connection.process_data_events( time_limit=0.25 ) - except Exception: + except Exception as e: + LOG.warn("Exception during consuming message. " + str(e)) self._cleanup() - raise if timeout is not None: timeout = expiration_time - time.time() if timeout <= 0: - return None + break - return self._message_queue.popleft() + result = self._message_queue[:prefetch_size] + self._message_queue = self._message_queue[prefetch_size:] + return result def start(self): """Starts poller. Should be called before polling to allow message @@ -226,7 +238,9 @@ class RpcServicePikaPoller(PikaPoller): self._target = target super(RpcServicePikaPoller, self).__init__( - pika_engine, prefetch_count=prefetch_count) + pika_engine, prefetch_count=prefetch_count, + incoming_message_class=pika_drv_msg.RpcPikaIncomingMessage + ) def _declare_queue_binding(self): """Overrides base method and perform declaration of RabbitMQ exchanges @@ -274,21 +288,6 @@ class RpcServicePikaPoller(PikaPoller): ) return queues_to_consume - def poll(self, timeout=None): - """Overrides base method and wrap RabbitMQ message into - RpcPikaIncomingMessage - - :param: timeout: float, seconds, timeout for waiting new incoming - message, None means wait forever - :return: RpcPikaIncomingMessage, consumed RPC message - """ - msg = super(RpcServicePikaPoller, self).poll(timeout) - if msg is None: - return None - return pika_drv_msg.RpcPikaIncomingMessage( - self._pika_engine, *msg - ) - class RpcReplyPikaPoller(PikaPoller): """PikaPoller implementation for polling RPC reply messages. Overrides @@ -309,7 +308,8 @@ class RpcReplyPikaPoller(PikaPoller): self._queue = queue super(RpcReplyPikaPoller, self).__init__( - pika_engine, prefetch_count + pika_engine=pika_engine, prefetch_count=prefetch_count, + incoming_message_class=pika_drv_msg.RpcReplyPikaIncomingMessage ) def _declare_queue_binding(self): @@ -355,21 +355,6 @@ class RpcReplyPikaPoller(PikaPoller): retrier(self.reconnect)() - def poll(self, timeout=None): - """Overrides base method and wrap RabbitMQ message into - RpcReplyPikaIncomingMessage - - :param: timeout: float, seconds, timeout for waiting new incoming - message, None means wait forever - :return: RpcReplyPikaIncomingMessage, consumed RPC reply message - """ - msg = super(RpcReplyPikaPoller, self).poll(timeout) - if msg is None: - return None - return pika_drv_msg.RpcReplyPikaIncomingMessage( - self._pika_engine, *msg - ) - class NotificationPikaPoller(PikaPoller): """PikaPoller implementation for polling Notification messages. Overrides @@ -421,18 +406,3 @@ class NotificationPikaPoller(PikaPoller): queues_to_consume[queue] = False return queues_to_consume - - def poll(self, timeout=None): - """Overrides base method and wrap RabbitMQ message into - PikaIncomingMessage - - :param: timeout: float, seconds, timeout for waiting new incoming - message, None means wait forever - :return: PikaIncomingMessage, consumed Notification message - """ - msg = super(NotificationPikaPoller, self).poll(timeout) - if msg is None: - return None - return pika_drv_msg.PikaIncomingMessage( - self._pika_engine, *msg - ) From 5149461fd2523a0c2afc187bd023784438f7b81a Mon Sep 17 00:00:00 2001 From: Dmitriy Ukhlov Date: Mon, 14 Dec 2015 11:36:28 +0200 Subject: [PATCH 15/16] Adds tests for pika_message.py Also small mistakes were fixed, msg_id, unique_id and reply_q fields were moved to corresponding AMQP properties Change-Id: I5147c35c1a2ce0205e08ca81db164a3cc879fb0a --- oslo_messaging/_drivers/impl_pika.py | 4 +- .../_drivers/pika_driver/pika_engine.py | 12 +- .../_drivers/pika_driver/pika_message.py | 261 ++++---- .../_drivers/pika_driver/pika_poller.py | 3 +- oslo_messaging/tests/drivers/pika/__init__.py | 0 .../tests/drivers/pika/test_message.py | 622 ++++++++++++++++++ 6 files changed, 781 insertions(+), 121 deletions(-) create mode 100644 oslo_messaging/tests/drivers/pika/__init__.py create mode 100644 oslo_messaging/tests/drivers/pika/test_message.py diff --git a/oslo_messaging/_drivers/impl_pika.py b/oslo_messaging/_drivers/impl_pika.py index eaa4a7685..3d633a5b1 100644 --- a/oslo_messaging/_drivers/impl_pika.py +++ b/oslo_messaging/_drivers/impl_pika.py @@ -198,8 +198,8 @@ class PikaDriver(object): "Timeout for current operation was expired." ) try: - with self._pika_engine.connection_pool.acquire( - timeout=timeout) as conn: + with (self._pika_engine.connection_without_confirmation_pool + .acquire)(timeout=timeout) as conn: self._pika_engine.declare_queue_binding_by_channel( conn.channel, exchange=( diff --git a/oslo_messaging/_drivers/pika_driver/pika_engine.py b/oslo_messaging/_drivers/pika_driver/pika_engine.py index 06dfcdbf1..6e877bb2e 100644 --- a/oslo_messaging/_drivers/pika_driver/pika_engine.py +++ b/oslo_messaging/_drivers/pika_driver/pika_engine.py @@ -12,6 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. +import random import socket import sys import threading @@ -44,7 +45,7 @@ def _is_eventlet_monkey_patched(module): return eventlet.patcher.is_monkey_patched(module) -def _create__select_poller_connection_impl( +def _create_select_poller_connection_impl( parameters, on_open_callback, on_open_error_callback, on_close_callback, stop_ioloop_on_close): """Used for disabling autochoise of poller ('select', 'poll', 'epool', etc) @@ -198,7 +199,6 @@ class PikaEngine(object): self._connection_host_param_list = [] self._connection_host_status_list = [] - self._next_connection_host_num = 0 for transport_host in url.hosts: pika_params = common_pika_params.copy() @@ -215,9 +215,13 @@ class PikaEngine(object): self.HOST_CONNECTION_LAST_SUCCESS_TRY_TIME: 0 }) + self._next_connection_host_num = random.randint( + 0, len(self._connection_host_param_list) - 1 + ) + # initializing 2 connection pools: 1st for connections without # confirmations, 2nd - with confirmations - self.connection_pool = pika_pool.QueuedPool( + self.connection_without_confirmation_pool = pika_pool.QueuedPool( create=self.create_connection, max_size=self.conf.oslo_messaging_pika.pool_max_size, max_overflow=self.conf.oslo_messaging_pika.pool_max_overflow, @@ -336,7 +340,7 @@ class PikaEngine(object): ), **base_host_params ), - _impl_class=(_create__select_poller_connection_impl + _impl_class=(_create_select_poller_connection_impl if self._force_select_poller_use else None) ) diff --git a/oslo_messaging/_drivers/pika_driver/pika_message.py b/oslo_messaging/_drivers/pika_driver/pika_message.py index 9bf9febdb..edd5c7328 100644 --- a/oslo_messaging/_drivers/pika_driver/pika_message.py +++ b/oslo_messaging/_drivers/pika_driver/pika_message.py @@ -95,40 +95,36 @@ class PikaIncomingMessage(object): self._pika_engine = pika_engine self._no_ack = no_ack self._channel = channel - self.delivery_tag = method.delivery_tag + self._delivery_tag = method.delivery_tag - self.version = version + self._version = version - self.content_type = getattr(properties, "content_type", - "application/json") - self.content_encoding = getattr(properties, "content_encoding", - "utf-8") + self._content_type = properties.content_type + self._content_encoding = properties.content_encoding + self.unique_id = properties.message_id self.expiration_time = ( None if properties.expiration is None else time.time() + float(properties.expiration) / 1000 ) - if self.content_type != "application/json": + if self._content_type != "application/json": raise NotImplementedError( "Content-type['{}'] is not valid, " "'application/json' only is supported.".format( - self.content_type + self._content_type ) ) - message_dict = jsonutils.loads(body, encoding=self.content_encoding) + message_dict = jsonutils.loads(body, encoding=self._content_encoding) context_dict = {} for key in list(message_dict.keys()): key = six.text_type(key) - if key.startswith('_context_'): + if key.startswith('_$_'): value = message_dict.pop(key) - context_dict[key[9:]] = value - elif key.startswith('_'): - value = message_dict.pop(key) - setattr(self, key[1:], value) + context_dict[key[3:]] = value self.message = message_dict self.ctxt = context_dict @@ -138,7 +134,7 @@ class PikaIncomingMessage(object): message anymore) """ if not self._no_ack: - self._channel.basic_ack(delivery_tag=self.delivery_tag) + self._channel.basic_ack(delivery_tag=self._delivery_tag) def requeue(self): """Rollback the message. Should be called by message processing logic @@ -146,7 +142,7 @@ class PikaIncomingMessage(object): later if it is possible """ if not self._no_ack: - return self._channel.basic_nack(delivery_tag=self.delivery_tag, + return self._channel.basic_nack(delivery_tag=self._delivery_tag, requeue=True) @@ -170,58 +166,30 @@ class RpcPikaIncomingMessage(PikaIncomingMessage): :param no_ack: Boolean, defines should this message be acked by consumer or not """ - self.msg_id = None - self.reply_q = None - super(RpcPikaIncomingMessage, self).__init__( pika_engine, channel, method, properties, body, no_ack ) + self.reply_q = properties.reply_to + self.msg_id = properties.correlation_id def reply(self, reply=None, failure=None, log_failure=True): """Send back reply to the RPC client - :param reply - Dictionary, reply. In case of exception should be None - :param failure - Exception, exception, raised during processing RPC - request. Should be None if RPC request was successfully processed - :param log_failure, Boolean, not used in this implementation. + :param reply: Dictionary, reply. In case of exception should be None + :param failure: Tuple, should be a sys.exc_info() tuple. + Should be None if RPC request was successfully processed. + :param log_failure: Boolean, not used in this implementation. It present here to be compatible with driver API + + :return RpcReplyPikaIncomingMessage, message with reply """ - if not (self.msg_id and self.reply_q): + + if self.reply_q is None: return - msg = { - '_msg_id': self.msg_id, - } - - if failure is not None: - if isinstance(failure, RemoteExceptionMixin): - failure_data = { - 'class': failure.clazz, - 'module': failure.module, - 'message': failure.message, - 'tb': failure.trace - } - else: - tb = traceback.format_exception(*failure) - failure = failure[1] - - cls_name = six.text_type(failure.__class__.__name__) - mod_name = six.text_type(failure.__class__.__module__) - - failure_data = { - 'class': cls_name, - 'module': mod_name, - 'message': six.text_type(failure), - 'tb': tb - } - - msg['_failure'] = failure_data - - if reply is not None: - msg['_result'] = reply - - reply_outgoing_message = PikaOutgoingMessage( - self._pika_engine, msg, self.ctxt, content_type=self.content_type, - content_encoding=self.content_encoding + reply_outgoing_message = RpcReplyPikaOutgoingMessage( + self._pika_engine, self.msg_id, reply=reply, failure_info=failure, + content_type=self._content_type, + content_encoding=self._content_encoding ) def on_exception(ex): @@ -242,11 +210,7 @@ class RpcPikaIncomingMessage(PikaIncomingMessage): try: reply_outgoing_message.send( - exchange=self._pika_engine.rpc_reply_exchange, - routing_key=self.reply_q, - confirm=True, - mandatory=False, - persistent=False, + reply_q=self.reply_q, expiration_time=self.expiration_time, retrier=retrier ) @@ -282,18 +246,20 @@ class RpcReplyPikaIncomingMessage(PikaIncomingMessage): :param no_ack: Boolean, defines should this message be acked by consumer or not """ - self.result = None - self.failure = None - super(RpcReplyPikaIncomingMessage, self).__init__( pika_engine, channel, method, properties, body, no_ack ) + self.msg_id = properties.correlation_id + + self.result = self.message.get("s", None) + self.failure = self.message.get("e", None) + if self.failure is not None: - trace = self.failure.get('tb', []) - message = self.failure.get('message', "") - class_name = self.failure.get('class') - module_name = self.failure.get('module') + trace = self.failure.get('t', []) + message = self.failure.get('s', "") + class_name = self.failure.get('c') + module_name = self.failure.get('m') res_exc = None @@ -343,14 +309,14 @@ class PikaOutgoingMessage(object): self._pika_engine = pika_engine - self.content_type = content_type - self.content_encoding = content_encoding + self._content_type = content_type + self._content_encoding = content_encoding - if self.content_type != "application/json": + if self._content_type != "application/json": raise NotImplementedError( "Content-type['{}'] is not valid, " "'application/json' only is supported.".format( - self.content_type + self._content_type ) ) @@ -362,23 +328,21 @@ class PikaOutgoingMessage(object): def _prepare_message_to_send(self): """Combine user's message fields an system fields (_unique_id, context's data etc) - - :param pika_engine: PikaEngine, shared object with configuration and - shared driver functionality - :param message: Dictionary, user's message fields - :param context: Dictionary, request context's fields - :param content_type: String, content-type header, defines serialization - mechanism - :param content_encoding: String, defines encoding for text data """ msg = self.message.copy() - msg['_unique_id'] = self.unique_id + if self.context: + for key, value in six.iteritems(self.context): + key = six.text_type(key) + msg['_$_' + key] = value - for key, value in self.context.iteritems(): - key = six.text_type(key) - msg['_context_' + key] = value - return msg + props = pika_spec.BasicProperties( + content_encoding=self._content_encoding, + content_type=self._content_type, + headers={_VERSION_HEADER: _VERSION}, + message_id=self.unique_id, + ) + return msg, props @staticmethod def _publish(pool, exchange, routing_key, body, properties, mandatory, @@ -456,14 +420,15 @@ class PikaOutgoingMessage(object): "Socket timeout exceeded." ) - def _do_send(self, exchange, routing_key, msg_dict, confirm=True, - mandatory=True, persistent=False, expiration_time=None, - retrier=None): + def _do_send(self, exchange, routing_key, msg_dict, msg_props, + confirm=True, mandatory=True, persistent=False, + expiration_time=None, retrier=None): """Send prepared message with configured retrying :param exchange: String, RabbitMQ exchange name for message sending :param routing_key: String, RabbitMQ routing key for message routing :param msg_dict: Dictionary, message payload + :param msg_props: Properties, message properties :param confirm: Boolean, enable publisher confirmation if True :param mandatory: Boolean, RabbitMQ publish mandatory flag (raise exception if it is not possible to deliver message to any queue) @@ -474,29 +439,26 @@ class PikaOutgoingMessage(object): :param retrier: retrying.Retrier, configured retrier object for sending message, if None no retrying is performed """ - properties = pika_spec.BasicProperties( - content_encoding=self.content_encoding, - content_type=self.content_type, - headers={_VERSION_HEADER: _VERSION}, - delivery_mode=2 if persistent else 1 - ) + msg_props.delivery_mode = 2 if persistent else 1 pool = (self._pika_engine.connection_with_confirmation_pool - if confirm else self._pika_engine.connection_pool) + if confirm else + self._pika_engine.connection_without_confirmation_pool) - body = jsonutils.dumps(msg_dict, encoding=self.content_encoding) + body = jsonutils.dump_as_bytes(msg_dict, + encoding=self._content_encoding) LOG.debug( "Sending message:[body:{}; properties: {}] to target: " "[exchange:{}; routing_key:{}]".format( - body, properties, exchange, routing_key + body, msg_props, exchange, routing_key ) ) publish = (self._publish if retrier is None else retrier(self._publish)) - return publish(pool, exchange, routing_key, body, properties, + return publish(pool, exchange, routing_key, body, msg_props, mandatory, expiration_time) def send(self, exchange, routing_key='', confirm=True, mandatory=True, @@ -515,10 +477,11 @@ class PikaOutgoingMessage(object): :param retrier: retrying.Retrier, configured retrier object for sending message, if None no retrying is performed """ - msg_dict = self._prepare_message_to_send() + msg_dict, msg_props = self._prepare_message_to_send() - return self._do_send(exchange, routing_key, msg_dict, confirm, - mandatory, persistent, expiration_time, retrier) + return self._do_send(exchange, routing_key, msg_dict, msg_props, + confirm, mandatory, persistent, expiration_time, + retrier) class RpcPikaOutgoingMessage(PikaOutgoingMessage): @@ -554,23 +517,25 @@ class RpcPikaOutgoingMessage(PikaOutgoingMessage): target.topic, target.server, retrier is None ) - msg_dict = self._prepare_message_to_send() + msg_dict, msg_props = self._prepare_message_to_send() if reply_listener: - msg_id = uuid.uuid4().hex - msg_dict["_msg_id"] = msg_id - LOG.debug('MSG_ID is %s', msg_id) + self.msg_id = uuid.uuid4().hex + msg_props.correlation_id = self.msg_id + LOG.debug('MSG_ID is %s', self.msg_id) - msg_dict["_reply_q"] = reply_listener.get_reply_qname( + self.reply_q = reply_listener.get_reply_qname( expiration_time - time.time() ) + msg_props.reply_to = self.reply_q - future = reply_listener.register_reply_waiter(msg_id=msg_id) + future = reply_listener.register_reply_waiter(msg_id=self.msg_id) self._do_send( exchange=exchange, routing_key=queue, msg_dict=msg_dict, - confirm=True, mandatory=True, persistent=False, - expiration_time=expiration_time, retrier=retrier + msg_props=msg_props, confirm=True, mandatory=True, + persistent=False, expiration_time=expiration_time, + retrier=retrier ) try: @@ -580,10 +545,78 @@ class RpcPikaOutgoingMessage(PikaOutgoingMessage): if isinstance(e, futures.TimeoutError): e = exceptions.MessagingTimeout() raise e - else: self._do_send( exchange=exchange, routing_key=queue, msg_dict=msg_dict, - confirm=True, mandatory=True, persistent=False, - expiration_time=expiration_time, retrier=retrier + msg_props=msg_props, confirm=True, mandatory=True, + persistent=False, expiration_time=expiration_time, + retrier=retrier ) + + +class RpcReplyPikaOutgoingMessage(PikaOutgoingMessage): + """PikaOutgoingMessage implementation for RPC reply messages. It sets + correlation_id AMQP property to link this reply with response + """ + def __init__(self, pika_engine, msg_id, reply=None, failure_info=None, + content_type="application/json", content_encoding="utf-8"): + """Initialize with reply information for sending + + :param pika_engine: PikaEngine, shared object with configuration and + shared driver functionality + :param msg_id: String, msg_id of RPC request, which waits for reply + :param reply: Dictionary, reply. In case of exception should be None + :param failure_info: Tuple, should be a sys.exc_info() tuple. + Should be None if RPC request was successfully processed. + :param content_type: String, content-type header, defines serialization + mechanism + :param content_encoding: String, defines encoding for text data + """ + self.msg_id = msg_id + + if failure_info is not None: + ex_class = failure_info[0] + ex = failure_info[1] + tb = traceback.format_exception(*failure_info) + if issubclass(ex_class, RemoteExceptionMixin): + failure_data = { + 'c': ex.clazz, + 'm': ex.module, + 's': ex.message, + 't': tb + } + else: + failure_data = { + 'c': six.text_type(ex_class.__name__), + 'm': six.text_type(ex_class.__module__), + 's': six.text_type(ex), + 't': tb + } + + msg = {'e': failure_data} + else: + msg = {'s': reply} + + super(RpcReplyPikaOutgoingMessage, self).__init__( + pika_engine, msg, None, content_type, content_encoding + ) + + def send(self, reply_q, expiration_time=None, retrier=None): + """Send RPC message with configured retrying + + :param reply_q: String, queue name for sending reply + :param expiration_time: Float, expiration time in seconds + (like time.time()) + :param retrier: retrying.Retrier, configured retrier object for sending + message, if None no retrying is performed + """ + + msg_dict, msg_props = self._prepare_message_to_send() + msg_props.correlation_id = self.msg_id + + self._do_send( + exchange=self._pika_engine.rpc_reply_exchange, routing_key=reply_q, + msg_dict=msg_dict, msg_props=msg_props, confirm=True, + mandatory=True, persistent=False, expiration_time=expiration_time, + retrier=retrier + ) diff --git a/oslo_messaging/_drivers/pika_driver/pika_poller.py b/oslo_messaging/_drivers/pika_driver/pika_poller.py index 1390ced75..5aa948a2e 100644 --- a/oslo_messaging/_drivers/pika_driver/pika_poller.py +++ b/oslo_messaging/_drivers/pika_driver/pika_poller.py @@ -18,6 +18,7 @@ import time from oslo_log import log as logging import pika_pool import retrying +import six from oslo_messaging._drivers.pika_driver import pika_message as pika_drv_msg @@ -68,7 +69,7 @@ class PikaPoller(object): if self._queues_to_consume is None: self._queues_to_consume = self._declare_queue_binding() - for queue, no_ack in self._queues_to_consume.iteritems(): + for queue, no_ack in six.iteritems(self._queues_to_consume): self._start_consuming(queue, no_ack) def _declare_queue_binding(self): diff --git a/oslo_messaging/tests/drivers/pika/__init__.py b/oslo_messaging/tests/drivers/pika/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/oslo_messaging/tests/drivers/pika/test_message.py b/oslo_messaging/tests/drivers/pika/test_message.py new file mode 100644 index 000000000..5008ce36e --- /dev/null +++ b/oslo_messaging/tests/drivers/pika/test_message.py @@ -0,0 +1,622 @@ +# Copyright 2015 Mirantis, 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. +import functools +import time +import unittest + +from concurrent import futures +from mock import mock, patch +from oslo_serialization import jsonutils +import pika +from pika import spec + +import oslo_messaging +from oslo_messaging._drivers.pika_driver import pika_engine +from oslo_messaging._drivers.pika_driver import pika_message as pika_drv_msg + + +class PikaIncomingMessageTestCase(unittest.TestCase): + def setUp(self): + self._pika_engine = mock.Mock() + self._channel = mock.Mock() + + self._delivery_tag = 12345 + + self._method = pika.spec.Basic.Deliver(delivery_tag=self._delivery_tag) + self._properties = pika.BasicProperties( + content_type="application/json", + headers={"version": "1.0"}, + ) + self._body = ( + b'{"_$_key_context":"context_value",' + b'"payload_key": "payload_value"}' + ) + + def test_message_body_parsing(self): + message = pika_drv_msg.PikaIncomingMessage( + self._pika_engine, self._channel, self._method, self._properties, + self._body, True + ) + + self.assertEqual(message.ctxt.get("key_context", None), + "context_value") + self.assertEqual(message.message.get("payload_key", None), + "payload_value") + + def test_message_acknowledge(self): + message = pika_drv_msg.PikaIncomingMessage( + self._pika_engine, self._channel, self._method, self._properties, + self._body, False + ) + + message.acknowledge() + + self.assertEqual(1, self._channel.basic_ack.call_count) + self.assertEqual({"delivery_tag": self._delivery_tag}, + self._channel.basic_ack.call_args[1]) + + def test_message_acknowledge_no_ack(self): + message = pika_drv_msg.PikaIncomingMessage( + self._pika_engine, self._channel, self._method, self._properties, + self._body, True + ) + + message.acknowledge() + + self.assertEqual(0, self._channel.basic_ack.call_count) + + def test_message_requeue(self): + message = pika_drv_msg.PikaIncomingMessage( + self._pika_engine, self._channel, self._method, self._properties, + self._body, False + ) + + message.requeue() + + self.assertEqual(1, self._channel.basic_nack.call_count) + self.assertEqual({"delivery_tag": self._delivery_tag, 'requeue': True}, + self._channel.basic_nack.call_args[1]) + + def test_message_requeue_no_ack(self): + message = pika_drv_msg.PikaIncomingMessage( + self._pika_engine, self._channel, self._method, self._properties, + self._body, True + ) + + message.requeue() + + self.assertEqual(0, self._channel.basic_nack.call_count) + + +class RpcPikaIncomingMessageTestCase(unittest.TestCase): + def setUp(self): + self._pika_engine = mock.Mock() + self._pika_engine.rpc_reply_retry_attempts = 3 + self._pika_engine.rpc_reply_retry_delay = 0.25 + + self._channel = mock.Mock() + + self._delivery_tag = 12345 + + self._method = pika.spec.Basic.Deliver(delivery_tag=self._delivery_tag) + self._body = ( + b'{"_$_key_context":"context_value",' + b'"payload_key":"payload_value"}' + ) + self._properties = pika.BasicProperties( + content_type="application/json", + content_encoding="utf-8", + headers={"version": "1.0"}, + ) + + def test_call_message_body_parsing(self): + self._properties.correlation_id = 123456789 + self._properties.reply_to = "reply_queue" + + message = pika_drv_msg.RpcPikaIncomingMessage( + self._pika_engine, self._channel, self._method, self._properties, + self._body, True + ) + + self.assertEqual(message.ctxt.get("key_context", None), + "context_value") + self.assertEqual(message.msg_id, 123456789) + self.assertEqual(message.reply_q, "reply_queue") + + self.assertEqual(message.message.get("payload_key", None), + "payload_value") + + def test_cast_message_body_parsing(self): + message = pika_drv_msg.RpcPikaIncomingMessage( + self._pika_engine, self._channel, self._method, self._properties, + self._body, True + ) + + self.assertEqual(message.ctxt.get("key_context", None), + "context_value") + self.assertEqual(message.msg_id, None) + self.assertEqual(message.reply_q, None) + + self.assertEqual(message.message.get("payload_key", None), + "payload_value") + + @patch(("oslo_messaging._drivers.pika_driver.pika_message." + "PikaOutgoingMessage.send")) + def test_reply_for_cast_message(self, send_reply_mock): + message = pika_drv_msg.RpcPikaIncomingMessage( + self._pika_engine, self._channel, self._method, self._properties, + self._body, True + ) + + self.assertEqual(message.ctxt.get("key_context", None), + "context_value") + self.assertEqual(message.msg_id, None) + self.assertEqual(message.reply_q, None) + + self.assertEqual(message.message.get("payload_key", None), + "payload_value") + + message.reply(reply=object()) + + self.assertEqual(send_reply_mock.call_count, 0) + + @patch("oslo_messaging._drivers.pika_driver.pika_message." + "RpcReplyPikaOutgoingMessage") + @patch("retrying.retry") + def test_positive_reply_for_call_message(self, + retry_mock, + outgoing_message_mock): + self._properties.correlation_id = 123456789 + self._properties.reply_to = "reply_queue" + + message = pika_drv_msg.RpcPikaIncomingMessage( + self._pika_engine, self._channel, self._method, self._properties, + self._body, True + ) + + self.assertEqual(message.ctxt.get("key_context", None), + "context_value") + self.assertEqual(message.msg_id, 123456789) + self.assertEqual(message.reply_q, "reply_queue") + + self.assertEqual(message.message.get("payload_key", None), + "payload_value") + reply = "all_fine" + message.reply(reply=reply) + + outgoing_message_mock.assert_called_once_with( + self._pika_engine, 123456789, failure_info=None, reply='all_fine', + content_encoding='utf-8', content_type='application/json' + ) + outgoing_message_mock().send.assert_called_once_with( + expiration_time=None, reply_q='reply_queue', retrier=mock.ANY + ) + retry_mock.assert_called_once_with( + retry_on_exception=mock.ANY, stop_max_attempt_number=3, + wait_fixed=250.0 + ) + + @patch("oslo_messaging._drivers.pika_driver.pika_message." + "RpcReplyPikaOutgoingMessage") + @patch("retrying.retry") + def test_negative_reply_for_call_message(self, + retry_mock, + outgoing_message_mock): + self._properties.correlation_id = 123456789 + self._properties.reply_to = "reply_queue" + + message = pika_drv_msg.RpcPikaIncomingMessage( + self._pika_engine, self._channel, self._method, self._properties, + self._body, True + ) + + self.assertEqual(message.ctxt.get("key_context", None), + "context_value") + self.assertEqual(message.msg_id, 123456789) + self.assertEqual(message.reply_q, "reply_queue") + + self.assertEqual(message.message.get("payload_key", None), + "payload_value") + + failure_info = object() + message.reply(failure=failure_info) + + outgoing_message_mock.assert_called_once_with( + self._pika_engine, 123456789, + failure_info=failure_info, + reply=None, + content_encoding='utf-8', + content_type='application/json' + ) + outgoing_message_mock().send.assert_called_once_with( + expiration_time=None, reply_q='reply_queue', retrier=mock.ANY + ) + retry_mock.assert_called_once_with( + retry_on_exception=mock.ANY, stop_max_attempt_number=3, + wait_fixed=250.0 + ) + + +class RpcReplyPikaIncomingMessageTestCase(unittest.TestCase): + def setUp(self): + self._pika_engine = mock.Mock() + self._pika_engine.allowed_remote_exmods = [ + pika_engine._EXCEPTIONS_MODULE, "oslo_messaging.exceptions" + ] + + self._channel = mock.Mock() + + self._delivery_tag = 12345 + + self._method = pika.spec.Basic.Deliver(delivery_tag=self._delivery_tag) + + self._properties = pika.BasicProperties( + content_type="application/json", + content_encoding="utf-8", + headers={"version": "1.0"}, + correlation_id=123456789 + ) + + def test_positive_reply_message_body_parsing(self): + + body = b'{"s": "all fine"}' + + message = pika_drv_msg.RpcReplyPikaIncomingMessage( + self._pika_engine, self._channel, self._method, self._properties, + body, True + ) + + self.assertEqual(message.msg_id, 123456789) + self.assertIsNone(message.failure) + self.assertEquals(message.result, "all fine") + + def test_negative_reply_message_body_parsing(self): + + body = (b'{' + b' "e": {' + b' "s": "Error message",' + b' "t": ["TRACE HERE"],' + b' "c": "MessagingException",' + b' "m": "oslo_messaging.exceptions"' + b' }' + b'}') + + message = pika_drv_msg.RpcReplyPikaIncomingMessage( + self._pika_engine, self._channel, self._method, self._properties, + body, True + ) + + self.assertEqual(message.msg_id, 123456789) + self.assertIsNone(message.result) + self.assertEquals( + str(message.failure), + 'Error message\n' + 'TRACE HERE' + ) + self.assertIsInstance(message.failure, + oslo_messaging.MessagingException) + + +class PikaOutgoingMessageTestCase(unittest.TestCase): + def setUp(self): + self._pika_engine = mock.MagicMock() + self._exchange = "it is exchange" + self._routing_key = "it is routing key" + self._expiration = 1 + self._expiration_time = time.time() + self._expiration + self._mandatory = object() + + self._message = {"msg_type": 1, "msg_str": "hello"} + self._context = {"request_id": 555, "token": "it is a token"} + + @patch("oslo_serialization.jsonutils.dumps", + new=functools.partial(jsonutils.dumps, sort_keys=True)) + def test_send_with_confirmation(self): + message = pika_drv_msg.PikaOutgoingMessage( + self._pika_engine, self._message, self._context + ) + + message.send( + exchange=self._exchange, + routing_key=self._routing_key, + confirm=True, + mandatory=self._mandatory, + persistent=True, + expiration_time=self._expiration_time, + retrier=None + ) + + self._pika_engine.connection_with_confirmation_pool.acquire( + ).__enter__().channel.publish.assert_called_once_with( + body=mock.ANY, + exchange=self._exchange, mandatory=self._mandatory, + properties=mock.ANY, + routing_key=self._routing_key + ) + + body = self._pika_engine.connection_with_confirmation_pool.acquire( + ).__enter__().channel.publish.call_args[1]["body"] + + self.assertEqual( + b'{"_$_request_id": 555, "_$_token": "it is a token", ' + b'"msg_str": "hello", "msg_type": 1}', + body + ) + + props = self._pika_engine.connection_with_confirmation_pool.acquire( + ).__enter__().channel.publish.call_args[1]["properties"] + + self.assertEqual(props.content_encoding, 'utf-8') + self.assertEqual(props.content_type, 'application/json') + self.assertEqual(props.delivery_mode, 2) + self.assertTrue(self._expiration * 1000 - float(props.expiration) < + 100) + self.assertEqual(props.headers, {'version': '1.0'}) + self.assertTrue(props.message_id) + + @patch("oslo_serialization.jsonutils.dumps", + new=functools.partial(jsonutils.dumps, sort_keys=True)) + def test_send_without_confirmation(self): + message = pika_drv_msg.PikaOutgoingMessage( + self._pika_engine, self._message, self._context + ) + + message.send( + exchange=self._exchange, + routing_key=self._routing_key, + confirm=False, + mandatory=self._mandatory, + persistent=False, + expiration_time=self._expiration_time, + retrier=None + ) + + self._pika_engine.connection_without_confirmation_pool.acquire( + ).__enter__().channel.publish.assert_called_once_with( + body=mock.ANY, + exchange=self._exchange, mandatory=self._mandatory, + properties=mock.ANY, + routing_key=self._routing_key + ) + + body = self._pika_engine.connection_without_confirmation_pool.acquire( + ).__enter__().channel.publish.call_args[1]["body"] + + self.assertEqual( + b'{"_$_request_id": 555, "_$_token": "it is a token", ' + b'"msg_str": "hello", "msg_type": 1}', + body + ) + + props = self._pika_engine.connection_without_confirmation_pool.acquire( + ).__enter__().channel.publish.call_args[1]["properties"] + + self.assertEqual(props.content_encoding, 'utf-8') + self.assertEqual(props.content_type, 'application/json') + self.assertEqual(props.delivery_mode, 1) + self.assertTrue(self._expiration * 1000 - float(props.expiration) + < 100) + self.assertEqual(props.headers, {'version': '1.0'}) + self.assertTrue(props.message_id) + + +class RpcPikaOutgoingMessageTestCase(unittest.TestCase): + def setUp(self): + self._exchange = "it is exchange" + self._routing_key = "it is routing key" + + self._pika_engine = mock.MagicMock() + self._pika_engine.get_rpc_exchange_name.return_value = self._exchange + self._pika_engine.get_rpc_queue_name.return_value = self._routing_key + + self._message = {"msg_type": 1, "msg_str": "hello"} + self._context = {"request_id": 555, "token": "it is a token"} + + @patch("oslo_serialization.jsonutils.dumps", + new=functools.partial(jsonutils.dumps, sort_keys=True)) + def test_send_cast_message(self): + message = pika_drv_msg.RpcPikaOutgoingMessage( + self._pika_engine, self._message, self._context + ) + + expiration = 1 + expiration_time = time.time() + expiration + + message.send( + target=oslo_messaging.Target(exchange=self._exchange, + topic=self._routing_key), + reply_listener=None, + expiration_time=expiration_time, + retrier=None + ) + + self._pika_engine.connection_with_confirmation_pool.acquire( + ).__enter__().channel.publish.assert_called_once_with( + body=mock.ANY, + exchange=self._exchange, mandatory=True, + properties=mock.ANY, + routing_key=self._routing_key + ) + + body = self._pika_engine.connection_with_confirmation_pool.acquire( + ).__enter__().channel.publish.call_args[1]["body"] + + self.assertEqual( + b'{"_$_request_id": 555, "_$_token": "it is a token", ' + b'"msg_str": "hello", "msg_type": 1}', + body + ) + + props = self._pika_engine.connection_with_confirmation_pool.acquire( + ).__enter__().channel.publish.call_args[1]["properties"] + + self.assertEqual(props.content_encoding, 'utf-8') + self.assertEqual(props.content_type, 'application/json') + self.assertEqual(props.delivery_mode, 1) + self.assertTrue(expiration * 1000 - float(props.expiration) < 100) + self.assertEqual(props.headers, {'version': '1.0'}) + self.assertIsNone(props.correlation_id) + self.assertIsNone(props.reply_to) + self.assertTrue(props.message_id) + + @patch("oslo_serialization.jsonutils.dumps", + new=functools.partial(jsonutils.dumps, sort_keys=True)) + def test_send_call_message(self): + message = pika_drv_msg.RpcPikaOutgoingMessage( + self._pika_engine, self._message, self._context + ) + + expiration = 1 + expiration_time = time.time() + expiration + + result = "it is a result" + reply_queue_name = "reply_queue_name" + + future = futures.Future() + future.set_result(result) + reply_listener = mock.Mock() + reply_listener.register_reply_waiter.return_value = future + reply_listener.get_reply_qname.return_value = reply_queue_name + + res = message.send( + target=oslo_messaging.Target(exchange=self._exchange, + topic=self._routing_key), + reply_listener=reply_listener, + expiration_time=expiration_time, + retrier=None + ) + + self.assertEqual(result, res) + + self._pika_engine.connection_with_confirmation_pool.acquire( + ).__enter__().channel.publish.assert_called_once_with( + body=mock.ANY, + exchange=self._exchange, mandatory=True, + properties=mock.ANY, + routing_key=self._routing_key + ) + + body = self._pika_engine.connection_with_confirmation_pool.acquire( + ).__enter__().channel.publish.call_args[1]["body"] + + self.assertEqual( + b'{"_$_request_id": 555, "_$_token": "it is a token", ' + b'"msg_str": "hello", "msg_type": 1}', + body + ) + + props = self._pika_engine.connection_with_confirmation_pool.acquire( + ).__enter__().channel.publish.call_args[1]["properties"] + + self.assertEqual(props.content_encoding, 'utf-8') + self.assertEqual(props.content_type, 'application/json') + self.assertEqual(props.delivery_mode, 1) + self.assertTrue(expiration * 1000 - float(props.expiration) < 100) + self.assertEqual(props.headers, {'version': '1.0'}) + self.assertEqual(props.correlation_id, message.msg_id) + self.assertEquals(props.reply_to, reply_queue_name) + self.assertTrue(props.message_id) + + +class RpcReplyPikaOutgoingMessageTestCase(unittest.TestCase): + def setUp(self): + self._reply_q = "reply_queue_name" + + self._expiration = 1 + self._expiration_time = time.time() + self._expiration + + self._pika_engine = mock.MagicMock() + + self._rpc_reply_exchange = "rpc_reply_exchange" + self._pika_engine.rpc_reply_exchange = self._rpc_reply_exchange + + self._msg_id = 12345567 + + @patch("oslo_serialization.jsonutils.dumps", + new=functools.partial(jsonutils.dumps, sort_keys=True)) + def test_success_message_send(self): + message = pika_drv_msg.RpcReplyPikaOutgoingMessage( + self._pika_engine, self._msg_id, reply="all_fine" + ) + + message.send(self._reply_q, expiration_time=self._expiration_time, + retrier=None) + + self._pika_engine.connection_with_confirmation_pool.acquire( + ).__enter__().channel.publish.assert_called_once_with( + body=b'{"s": "all_fine"}', + exchange=self._rpc_reply_exchange, mandatory=True, + properties=mock.ANY, + routing_key=self._reply_q + ) + + props = self._pika_engine.connection_with_confirmation_pool.acquire( + ).__enter__().channel.publish.call_args[1]["properties"] + + self.assertEqual(props.content_encoding, 'utf-8') + self.assertEqual(props.content_type, 'application/json') + self.assertEqual(props.delivery_mode, 1) + self.assertTrue(self._expiration * 1000 - float(props.expiration) < + 100) + self.assertEqual(props.headers, {'version': '1.0'}) + self.assertEqual(props.correlation_id, message.msg_id) + self.assertIsNone(props.reply_to) + self.assertTrue(props.message_id) + + @patch("traceback.format_exception", new=lambda x,y,z:z) + @patch("oslo_serialization.jsonutils.dumps", + new=functools.partial(jsonutils.dumps, sort_keys=True)) + def test_failure_message_send(self): + failure_info = (oslo_messaging.MessagingException, + oslo_messaging.MessagingException("Error message"), + ['It is a trace']) + + + message = pika_drv_msg.RpcReplyPikaOutgoingMessage( + self._pika_engine, self._msg_id, failure_info=failure_info + ) + + message.send(self._reply_q, expiration_time=self._expiration_time, + retrier=None) + + self._pika_engine.connection_with_confirmation_pool.acquire( + ).__enter__().channel.publish.assert_called_once_with( + body=mock.ANY, + exchange=self._rpc_reply_exchange, + mandatory=True, + properties=mock.ANY, + routing_key=self._reply_q + ) + + body = self._pika_engine.connection_with_confirmation_pool.acquire( + ).__enter__().channel.publish.call_args[1]["body"] + self.assertEqual( + b'{"e": {"c": "MessagingException", ' + b'"m": "oslo_messaging.exceptions", "s": "Error message", ' + b'"t": ["It is a trace"]}}', + body + ) + + props = self._pika_engine.connection_with_confirmation_pool.acquire( + ).__enter__().channel.publish.call_args[1]["properties"] + + self.assertEqual(props.content_encoding, 'utf-8') + self.assertEqual(props.content_type, 'application/json') + self.assertEqual(props.delivery_mode, 1) + self.assertTrue(self._expiration * 1000 - float(props.expiration) < + 100) + self.assertEqual(props.headers, {'version': '1.0'}) + self.assertEqual(props.correlation_id, message.msg_id) + self.assertIsNone(props.reply_to) + self.assertTrue(props.message_id) From 83a08d4b7ed36019f17a070a43ba6e4863cafe34 Mon Sep 17 00:00:00 2001 From: Dmitriy Ukhlov Date: Mon, 14 Dec 2015 11:36:28 +0200 Subject: [PATCH 16/16] Adds unit tests for pika_poll module Change-Id: I69cc0e0302382ab45ba464bb5993300d44679106 --- .../_drivers/pika_driver/pika_engine.py | 3 + .../_drivers/pika_driver/pika_message.py | 33 +- .../_drivers/pika_driver/pika_poller.py | 79 ++- .../tests/drivers/pika/test_message.py | 28 +- .../tests/drivers/pika/test_poller.py | 536 ++++++++++++++++++ 5 files changed, 605 insertions(+), 74 deletions(-) create mode 100644 oslo_messaging/tests/drivers/pika/test_poller.py diff --git a/oslo_messaging/_drivers/pika_driver/pika_engine.py b/oslo_messaging/_drivers/pika_driver/pika_engine.py index 6e877bb2e..4f38295a8 100644 --- a/oslo_messaging/_drivers/pika_driver/pika_engine.py +++ b/oslo_messaging/_drivers/pika_driver/pika_engine.py @@ -200,6 +200,9 @@ class PikaEngine(object): self._connection_host_param_list = [] self._connection_host_status_list = [] + if not url.hosts: + raise ValueError("You should provide at least one RabbitMQ host") + for transport_host in url.hosts: pika_params = common_pika_params.copy() pika_params.update( diff --git a/oslo_messaging/_drivers/pika_driver/pika_message.py b/oslo_messaging/_drivers/pika_driver/pika_message.py index edd5c7328..eac2be938 100644 --- a/oslo_messaging/_drivers/pika_driver/pika_message.py +++ b/oslo_messaging/_drivers/pika_driver/pika_message.py @@ -72,18 +72,17 @@ class PikaIncomingMessage(object): information from RabbitMQ message and provide access to it """ - def __init__(self, pika_engine, channel, method, properties, body, no_ack): + def __init__(self, pika_engine, channel, method, properties, body): """Parse RabbitMQ message :param pika_engine: PikaEngine, shared object with configuration and shared driver functionality :param channel: Channel, RabbitMQ channel which was used for - this message delivery + this message delivery, used for sending ack back. + If None - ack is not required :param method: Method, RabbitMQ message method :param properties: Properties, RabbitMQ message properties :param body: Bytes, RabbitMQ message body - :param no_ack: Boolean, defines should this message be acked by - consumer or not """ headers = getattr(properties, "headers", {}) version = headers.get(_VERSION_HEADER, None) @@ -93,7 +92,6 @@ class PikaIncomingMessage(object): "{}".format(version, _VERSION)) self._pika_engine = pika_engine - self._no_ack = no_ack self._channel = channel self._delivery_tag = method.delivery_tag @@ -128,12 +126,15 @@ class PikaIncomingMessage(object): self.message = message_dict self.ctxt = context_dict + def need_ack(self): + return self._channel is not None + def acknowledge(self): """Ack the message. Should be called by message processing logic when it considered as consumed (means that we don't need redelivery of this message anymore) """ - if not self._no_ack: + if self.need_ack(): self._channel.basic_ack(delivery_tag=self._delivery_tag) def requeue(self): @@ -141,7 +142,7 @@ class PikaIncomingMessage(object): when it can not process the message right now and should be redelivered later if it is possible """ - if not self._no_ack: + if self.need_ack(): return self._channel.basic_nack(delivery_tag=self._delivery_tag, requeue=True) @@ -152,22 +153,21 @@ class RpcPikaIncomingMessage(PikaIncomingMessage): method added to allow consumer to send RPC reply back to the RPC client """ - def __init__(self, pika_engine, channel, method, properties, body, no_ack): + def __init__(self, pika_engine, channel, method, properties, body): """Defines default values of msg_id and reply_q fields and just call super.__init__ method :param pika_engine: PikaEngine, shared object with configuration and shared driver functionality :param channel: Channel, RabbitMQ channel which was used for - this message delivery + this message delivery, used for sending ack back. + If None - ack is not required :param method: Method, RabbitMQ message method :param properties: Properties, RabbitMQ message properties :param body: Bytes, RabbitMQ message body - :param no_ack: Boolean, defines should this message be acked by - consumer or not """ super(RpcPikaIncomingMessage, self).__init__( - pika_engine, channel, method, properties, body, no_ack + pika_engine, channel, method, properties, body ) self.reply_q = properties.reply_to self.msg_id = properties.correlation_id @@ -231,7 +231,7 @@ class RpcReplyPikaIncomingMessage(PikaIncomingMessage): """PikaIncomingMessage implementation for RPC reply messages. It expects extra RPC reply related fields in message body (result and failure). """ - def __init__(self, pika_engine, channel, method, properties, body, no_ack): + def __init__(self, pika_engine, channel, method, properties, body): """Defines default values of result and failure fields, call super.__init__ method and then construct Exception object if failure is not None @@ -239,15 +239,14 @@ class RpcReplyPikaIncomingMessage(PikaIncomingMessage): :param pika_engine: PikaEngine, shared object with configuration and shared driver functionality :param channel: Channel, RabbitMQ channel which was used for - this message delivery + this message delivery, used for sending ack back. + If None - ack is not required :param method: Method, RabbitMQ message method :param properties: Properties, RabbitMQ message properties :param body: Bytes, RabbitMQ message body - :param no_ack: Boolean, defines should this message be acked by - consumer or not """ super(RpcReplyPikaIncomingMessage, self).__init__( - pika_engine, channel, method, properties, body, no_ack + pika_engine, channel, method, properties, body ) self.msg_id = properties.correlation_id diff --git a/oslo_messaging/_drivers/pika_driver/pika_poller.py b/oslo_messaging/_drivers/pika_driver/pika_poller.py index 5aa948a2e..3533dad2f 100644 --- a/oslo_messaging/_drivers/pika_driver/pika_poller.py +++ b/oslo_messaging/_drivers/pika_driver/pika_poller.py @@ -31,8 +31,7 @@ class PikaPoller(object): connectivity related problem detected """ - def __init__(self, pika_engine, prefetch_count, - incoming_message_class=pika_drv_msg.PikaIncomingMessage): + def __init__(self, pika_engine, prefetch_count, incoming_message_class): """Initialize required fields :param pika_engine: PikaEngine, shared object with configuration and @@ -110,8 +109,7 @@ class PikaPoller(object): """ self._message_queue.append( self._incoming_message_class( - self._pika_engine, self._channel, method, properties, body, - True + self._pika_engine, None, method, properties, body ) ) @@ -121,8 +119,7 @@ class PikaPoller(object): """ self._message_queue.append( self._incoming_message_class( - self._pika_engine, self._channel, method, properties, body, - False + self._pika_engine, self._channel, method, properties, body ) ) @@ -146,6 +143,11 @@ class PikaPoller(object): LOG.exception("Unexpected error during closing connection") self._connection = None + for i in xrange(len(self._message_queue) - 1, -1, -1): + message = self._message_queue[i] + if message.need_ack(): + del self._message_queue[i] + def poll(self, timeout=None, prefetch_size=1): """Main method of this class - consumes message from RabbitMQ @@ -158,32 +160,29 @@ class PikaPoller(object): """ expiration_time = time.time() + timeout if timeout else None - while len(self._message_queue) < prefetch_size: + while True: with self._lock: - if not self._started: - return None - - try: - if self._channel is None: - self._reconnect() - # we need some time_limit here, not too small to avoid a - # lot of not needed iterations but not too large to release - # lock time to time and give a chance to perform another - # method waiting this lock - self._connection.process_data_events( - time_limit=0.25 - ) - except Exception as e: - LOG.warn("Exception during consuming message. " + str(e)) - self._cleanup() - if timeout is not None: - timeout = expiration_time - time.time() - if timeout <= 0: - break - - result = self._message_queue[:prefetch_size] - self._message_queue = self._message_queue[prefetch_size:] - return result + if timeout is not None: + timeout = expiration_time - time.time() + if (len(self._message_queue) < prefetch_size and + self._started and ((timeout is None) or timeout > 0)): + try: + if self._channel is None: + self._reconnect() + # we need some time_limit here, not too small to avoid + # a lot of not needed iterations but not too large to + # release lock time to time and give a chance to + # perform another method waiting this lock + self._connection.process_data_events( + time_limit=0.25 + ) + except pika_pool.Connection.connectivity_errors: + self._cleanup() + raise + else: + result = self._message_queue[:prefetch_size] + del self._message_queue[:prefetch_size] + return result def start(self): """Starts poller. Should be called before polling to allow message @@ -201,7 +200,6 @@ class PikaPoller(object): return self._started = False - self._cleanup() def reconnect(self): """Safe version of _reconnect. Performs reconnection to the broker.""" @@ -249,9 +247,7 @@ class RpcServicePikaPoller(PikaPoller): :return Dictionary, declared_queue_name -> no_ack_mode """ - queue_expiration = ( - self._pika_engine.conf.oslo_messaging_pika.rpc_queue_expiration - ) + queue_expiration = self._pika_engine.rpc_queue_expiration queues_to_consume = {} @@ -319,15 +315,11 @@ class RpcReplyPikaPoller(PikaPoller): :return Dictionary, declared_queue_name -> no_ack_mode """ - queue_expiration = ( - self._pika_engine.conf.oslo_messaging_pika.rpc_queue_expiration - ) - self._pika_engine.declare_queue_binding_by_channel( channel=self._channel, exchange=self._exchange, queue=self._queue, routing_key=self._queue, exchange_type='direct', - queue_expiration=queue_expiration, + queue_expiration=self._pika_engine.rpc_queue_expiration, durable=False ) @@ -363,8 +355,8 @@ class NotificationPikaPoller(PikaPoller): """ def __init__(self, pika_engine, targets_and_priorities, queue_name=None, prefetch_count=100): - """Adds exchange and queue parameter for declaring exchange and queue - used for RPC reply delivery + """Adds targets_and_priorities and queue_name parameter + for declaring exchanges and queues used for notification delivery :param pika_engine: PikaEngine, shared object with configuration and shared driver functionality @@ -379,7 +371,8 @@ class NotificationPikaPoller(PikaPoller): self._queue_name = queue_name super(NotificationPikaPoller, self).__init__( - pika_engine, prefetch_count=prefetch_count + pika_engine, prefetch_count=prefetch_count, + incoming_message_class=pika_drv_msg.PikaIncomingMessage ) def _declare_queue_binding(self): diff --git a/oslo_messaging/tests/drivers/pika/test_message.py b/oslo_messaging/tests/drivers/pika/test_message.py index 5008ce36e..3c3f87e39 100644 --- a/oslo_messaging/tests/drivers/pika/test_message.py +++ b/oslo_messaging/tests/drivers/pika/test_message.py @@ -46,7 +46,7 @@ class PikaIncomingMessageTestCase(unittest.TestCase): def test_message_body_parsing(self): message = pika_drv_msg.PikaIncomingMessage( self._pika_engine, self._channel, self._method, self._properties, - self._body, True + self._body ) self.assertEqual(message.ctxt.get("key_context", None), @@ -57,7 +57,7 @@ class PikaIncomingMessageTestCase(unittest.TestCase): def test_message_acknowledge(self): message = pika_drv_msg.PikaIncomingMessage( self._pika_engine, self._channel, self._method, self._properties, - self._body, False + self._body ) message.acknowledge() @@ -68,8 +68,8 @@ class PikaIncomingMessageTestCase(unittest.TestCase): def test_message_acknowledge_no_ack(self): message = pika_drv_msg.PikaIncomingMessage( - self._pika_engine, self._channel, self._method, self._properties, - self._body, True + self._pika_engine, None, self._method, self._properties, + self._body ) message.acknowledge() @@ -79,7 +79,7 @@ class PikaIncomingMessageTestCase(unittest.TestCase): def test_message_requeue(self): message = pika_drv_msg.PikaIncomingMessage( self._pika_engine, self._channel, self._method, self._properties, - self._body, False + self._body ) message.requeue() @@ -90,8 +90,8 @@ class PikaIncomingMessageTestCase(unittest.TestCase): def test_message_requeue_no_ack(self): message = pika_drv_msg.PikaIncomingMessage( - self._pika_engine, self._channel, self._method, self._properties, - self._body, True + self._pika_engine, None, self._method, self._properties, + self._body ) message.requeue() @@ -126,7 +126,7 @@ class RpcPikaIncomingMessageTestCase(unittest.TestCase): message = pika_drv_msg.RpcPikaIncomingMessage( self._pika_engine, self._channel, self._method, self._properties, - self._body, True + self._body ) self.assertEqual(message.ctxt.get("key_context", None), @@ -140,7 +140,7 @@ class RpcPikaIncomingMessageTestCase(unittest.TestCase): def test_cast_message_body_parsing(self): message = pika_drv_msg.RpcPikaIncomingMessage( self._pika_engine, self._channel, self._method, self._properties, - self._body, True + self._body ) self.assertEqual(message.ctxt.get("key_context", None), @@ -156,7 +156,7 @@ class RpcPikaIncomingMessageTestCase(unittest.TestCase): def test_reply_for_cast_message(self, send_reply_mock): message = pika_drv_msg.RpcPikaIncomingMessage( self._pika_engine, self._channel, self._method, self._properties, - self._body, True + self._body ) self.assertEqual(message.ctxt.get("key_context", None), @@ -182,7 +182,7 @@ class RpcPikaIncomingMessageTestCase(unittest.TestCase): message = pika_drv_msg.RpcPikaIncomingMessage( self._pika_engine, self._channel, self._method, self._properties, - self._body, True + self._body ) self.assertEqual(message.ctxt.get("key_context", None), @@ -218,7 +218,7 @@ class RpcPikaIncomingMessageTestCase(unittest.TestCase): message = pika_drv_msg.RpcPikaIncomingMessage( self._pika_engine, self._channel, self._method, self._properties, - self._body, True + self._body ) self.assertEqual(message.ctxt.get("key_context", None), @@ -274,7 +274,7 @@ class RpcReplyPikaIncomingMessageTestCase(unittest.TestCase): message = pika_drv_msg.RpcReplyPikaIncomingMessage( self._pika_engine, self._channel, self._method, self._properties, - body, True + body ) self.assertEqual(message.msg_id, 123456789) @@ -294,7 +294,7 @@ class RpcReplyPikaIncomingMessageTestCase(unittest.TestCase): message = pika_drv_msg.RpcReplyPikaIncomingMessage( self._pika_engine, self._channel, self._method, self._properties, - body, True + body ) self.assertEqual(message.msg_id, 123456789) diff --git a/oslo_messaging/tests/drivers/pika/test_poller.py b/oslo_messaging/tests/drivers/pika/test_poller.py new file mode 100644 index 000000000..77a3b6b29 --- /dev/null +++ b/oslo_messaging/tests/drivers/pika/test_poller.py @@ -0,0 +1,536 @@ +# Copyright 2015 Mirantis, 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. + +import time +import unittest + +import mock + +from oslo_messaging._drivers.pika_driver import pika_poller + + +class PikaPollerTestCase(unittest.TestCase): + def setUp(self): + self._pika_engine = mock.Mock() + self._poller_connection_mock = mock.Mock() + self._poller_channel_mock = mock.Mock() + self._poller_connection_mock.channel.return_value = ( + self._poller_channel_mock + ) + self._pika_engine.create_connection.return_value = ( + self._poller_connection_mock + ) + self._prefetch_count = 123 + + @mock.patch("oslo_messaging._drivers.pika_driver.pika_poller.PikaPoller." + "_declare_queue_binding") + def test_poll(self, declare_queue_binding_mock): + incoming_message_class_mock = mock.Mock() + poller = pika_poller.PikaPoller( + self._pika_engine, self._prefetch_count, + incoming_message_class=incoming_message_class_mock + ) + unused = object() + method = object() + properties = object() + body = object() + + self._poller_connection_mock.process_data_events.side_effect = ( + lambda time_limit: poller._on_message_with_ack_callback( + unused, method, properties, body + ) + ) + + poller.start() + res = poller.poll() + + self.assertEqual(len(res), 1) + + self.assertEqual(res[0], incoming_message_class_mock.return_value) + incoming_message_class_mock.assert_called_once_with( + self._pika_engine, self._poller_channel_mock, method, properties, + body + ) + + self.assertTrue(self._pika_engine.create_connection.called) + self.assertTrue(self._poller_connection_mock.channel.called) + + self.assertTrue(declare_queue_binding_mock.called) + + @mock.patch("oslo_messaging._drivers.pika_driver.pika_poller.PikaPoller." + "_declare_queue_binding") + def test_poll_after_stop(self, declare_queue_binding_mock): + incoming_message_class_mock = mock.Mock() + poller = pika_poller.PikaPoller( + self._pika_engine, self._prefetch_count, + incoming_message_class=incoming_message_class_mock + ) + + n = 10 + params = [] + + for i in range(n): + params.append((object(), object(), object(), object())) + + index = [0] + + def f(time_limit): + for i in range(10): + poller._on_message_no_ack_callback( + *params[index[0]] + ) + index[0] += 1 + + self._poller_connection_mock.process_data_events.side_effect = f + + poller.start() + res = poller.poll(prefetch_size=1) + self.assertEqual(len(res), 1) + self.assertEqual(res[0], incoming_message_class_mock.return_value) + self.assertEqual( + incoming_message_class_mock.call_args_list[0][0], + (self._pika_engine, None) + params[0][1:] + ) + + poller.stop() + + res2 = poller.poll(prefetch_size=n) + + self.assertEqual(len(res2), n-1) + self.assertEqual(incoming_message_class_mock.call_count, n) + + self.assertEqual( + self._poller_connection_mock.process_data_events.call_count, 1) + + for i in range(n-1): + self.assertEqual(res2[i], incoming_message_class_mock.return_value) + self.assertEqual( + incoming_message_class_mock.call_args_list[i+1][0], + (self._pika_engine, None) + params[i+1][1:] + ) + + self.assertTrue(self._pika_engine.create_connection.called) + self.assertTrue(self._poller_connection_mock.channel.called) + + self.assertTrue(declare_queue_binding_mock.called) + + @mock.patch("oslo_messaging._drivers.pika_driver.pika_poller.PikaPoller." + "_declare_queue_binding") + def test_poll_batch(self, declare_queue_binding_mock): + incoming_message_class_mock = mock.Mock() + poller = pika_poller.PikaPoller( + self._pika_engine, self._prefetch_count, + incoming_message_class=incoming_message_class_mock + ) + + n = 10 + params = [] + + for i in range(n): + params.append((object(), object(), object(), object())) + + index = [0] + + def f(time_limit): + poller._on_message_with_ack_callback( + *params[index[0]] + ) + index[0] += 1 + + self._poller_connection_mock.process_data_events.side_effect = f + + poller.start() + res = poller.poll(prefetch_size=n) + + self.assertEqual(len(res), n) + self.assertEqual(incoming_message_class_mock.call_count, n) + + for i in range(n): + self.assertEqual(res[i], incoming_message_class_mock.return_value) + self.assertEqual( + incoming_message_class_mock.call_args_list[i][0], + (self._pika_engine, self._poller_channel_mock) + params[i][1:] + ) + + self.assertTrue(self._pika_engine.create_connection.called) + self.assertTrue(self._poller_connection_mock.channel.called) + + self.assertTrue(declare_queue_binding_mock.called) + + @mock.patch("oslo_messaging._drivers.pika_driver.pika_poller.PikaPoller." + "_declare_queue_binding") + def test_poll_batch_with_timeout(self, declare_queue_binding_mock): + incoming_message_class_mock = mock.Mock() + poller = pika_poller.PikaPoller( + self._pika_engine, self._prefetch_count, + incoming_message_class=incoming_message_class_mock + ) + + n = 10 + timeout = 1 + sleep_time = 0.2 + params = [] + + success_count = 5 + + for i in range(n): + params.append((object(), object(), object(), object())) + + index = [0] + + def f(time_limit): + time.sleep(sleep_time) + poller._on_message_with_ack_callback( + *params[index[0]] + ) + index[0] += 1 + + self._poller_connection_mock.process_data_events.side_effect = f + + poller.start() + res = poller.poll(prefetch_size=n, timeout=timeout) + + self.assertEqual(len(res), success_count) + self.assertEqual(incoming_message_class_mock.call_count, success_count) + + for i in range(success_count): + self.assertEqual(res[i], incoming_message_class_mock.return_value) + self.assertEqual( + incoming_message_class_mock.call_args_list[i][0], + (self._pika_engine, self._poller_channel_mock) + params[i][1:] + ) + + self.assertTrue(self._pika_engine.create_connection.called) + self.assertTrue(self._poller_connection_mock.channel.called) + + self.assertTrue(declare_queue_binding_mock.called) + + +class RpcServicePikaPollerTestCase(unittest.TestCase): + def setUp(self): + self._pika_engine = mock.Mock() + self._poller_connection_mock = mock.Mock() + self._poller_channel_mock = mock.Mock() + self._poller_connection_mock.channel.return_value = ( + self._poller_channel_mock + ) + self._pika_engine.create_connection.return_value = ( + self._poller_connection_mock + ) + + self._pika_engine.get_rpc_queue_name.side_effect = ( + lambda topic, server, no_ack: "_".join( + [topic, str(server), str(no_ack)] + ) + ) + + self._pika_engine.get_rpc_exchange_name.side_effect = ( + lambda exchange, topic, fanout, no_ack: "_".join( + [exchange, topic, str(fanout), str(no_ack)] + ) + ) + + self._prefetch_count = 123 + self._target = mock.Mock(exchange="exchange", topic="topic", + server="server") + self._pika_engine.rpc_queue_expiration = 12345 + + @mock.patch("oslo_messaging._drivers.pika_driver.pika_message." + "RpcPikaIncomingMessage") + def test_declare_rpc_queue_bindings(self, rpc_pika_incoming_message_mock): + poller = pika_poller.RpcServicePikaPoller( + self._pika_engine, self._target, self._prefetch_count, + ) + self._poller_connection_mock.process_data_events.side_effect = ( + lambda time_limit: poller._on_message_with_ack_callback( + None, None, None, None + ) + ) + + poller.start() + res = poller.poll() + + self.assertEqual(len(res), 1) + + self.assertEqual(res[0], rpc_pika_incoming_message_mock.return_value) + + self.assertTrue(self._pika_engine.create_connection.called) + self.assertTrue(self._poller_connection_mock.channel.called) + + declare_queue_binding_by_channel_mock = ( + self._pika_engine.declare_queue_binding_by_channel + ) + + self.assertEqual( + declare_queue_binding_by_channel_mock.call_count, 6 + ) + + declare_queue_binding_by_channel_mock.assert_has_calls(( + mock.call( + channel=self._poller_channel_mock, durable=False, + exchange="exchange_topic_False_True", + exchange_type='direct', + queue="topic_None_True", + queue_expiration=12345, + routing_key="topic_None_True" + ), + mock.call( + channel=self._poller_channel_mock, durable=False, + exchange="exchange_topic_False_True", + exchange_type='direct', + queue="topic_server_True", + queue_expiration=12345, + routing_key="topic_server_True" + ), + mock.call( + channel=self._poller_channel_mock, durable=False, + exchange="exchange_topic_True_True", + exchange_type='fanout', + queue="topic_server_True", + queue_expiration=12345, + routing_key='' + ), + mock.call( + channel=self._poller_channel_mock, durable=False, + exchange="exchange_topic_False_False", + exchange_type='direct', + queue="topic_None_False", + queue_expiration=12345, + routing_key="topic_None_False" + ), + mock.call( + channel=self._poller_channel_mock, durable=False, + exchange="exchange_topic_False_False", + exchange_type='direct', + queue="topic_server_False", + queue_expiration=12345, + routing_key="topic_server_False" + ), + mock.call( + channel=self._poller_channel_mock, durable=False, + exchange="exchange_topic_True_False", + exchange_type='fanout', + queue="topic_server_False", + queue_expiration=12345, + routing_key='' + ), + )) + + +class RpcReplyServicePikaPollerTestCase(unittest.TestCase): + def setUp(self): + self._pika_engine = mock.Mock() + self._poller_connection_mock = mock.Mock() + self._poller_channel_mock = mock.Mock() + self._poller_connection_mock.channel.return_value = ( + self._poller_channel_mock + ) + self._pika_engine.create_connection.return_value = ( + self._poller_connection_mock + ) + + self._prefetch_count = 123 + self._exchange = "rpc_reply_exchange" + self._queue = "rpc_reply_queue" + + self._pika_engine.rpc_reply_retry_delay = 12132543456 + + self._pika_engine.rpc_queue_expiration = 12345 + self._pika_engine.rpc_reply_retry_attempts = 3 + + def test_start(self): + poller = pika_poller.RpcReplyPikaPoller( + self._pika_engine, self._exchange, self._queue, + self._prefetch_count, + ) + + poller.start() + + self.assertTrue(self._pika_engine.create_connection.called) + self.assertTrue(self._poller_connection_mock.channel.called) + + def test_declare_rpc_reply_queue_binding(self): + poller = pika_poller.RpcReplyPikaPoller( + self._pika_engine, self._exchange, self._queue, + self._prefetch_count, + ) + + poller.start() + + declare_queue_binding_by_channel_mock = ( + self._pika_engine.declare_queue_binding_by_channel + ) + + self.assertEqual( + declare_queue_binding_by_channel_mock.call_count, 1 + ) + + declare_queue_binding_by_channel_mock.assert_called_once_with( + channel=self._poller_channel_mock, durable=False, + exchange='rpc_reply_exchange', exchange_type='direct', + queue='rpc_reply_queue', queue_expiration=12345, + routing_key='rpc_reply_queue' + ) + + +class NotificationPikaPollerTestCase(unittest.TestCase): + def setUp(self): + self._pika_engine = mock.Mock() + self._poller_connection_mock = mock.Mock() + self._poller_channel_mock = mock.Mock() + self._poller_connection_mock.channel.return_value = ( + self._poller_channel_mock + ) + self._pika_engine.create_connection.return_value = ( + self._poller_connection_mock + ) + + self._prefetch_count = 123 + self._target_and_priorities = ( + ( + mock.Mock(exchange="exchange1", topic="topic1", + server="server1"), 1 + ), + ( + mock.Mock(exchange="exchange1", topic="topic1"), 2 + ), + ( + mock.Mock(exchange="exchange2", topic="topic2",), 1 + ), + ) + self._pika_engine.notification_persistence = object() + + @mock.patch("oslo_messaging._drivers.pika_driver.pika_message." + "PikaIncomingMessage") + def test_declare_notification_queue_bindings_default_queue( + self, pika_incoming_message_mock): + poller = pika_poller.NotificationPikaPoller( + self._pika_engine, self._target_and_priorities, None, + self._prefetch_count, + ) + self._poller_connection_mock.process_data_events.side_effect = ( + lambda time_limit: poller._on_message_with_ack_callback( + None, None, None, None + ) + ) + + poller.start() + res = poller.poll() + + self.assertEqual(len(res), 1) + + self.assertEqual(res[0], pika_incoming_message_mock.return_value) + + self.assertTrue(self._pika_engine.create_connection.called) + self.assertTrue(self._poller_connection_mock.channel.called) + + declare_queue_binding_by_channel_mock = ( + self._pika_engine.declare_queue_binding_by_channel + ) + + self.assertEqual( + declare_queue_binding_by_channel_mock.call_count, 3 + ) + + declare_queue_binding_by_channel_mock.assert_has_calls(( + mock.call( + channel=self._poller_channel_mock, + durable=self._pika_engine.notification_persistence, + exchange="exchange1", + exchange_type='direct', + queue="topic1.1", + queue_expiration=None, + routing_key="topic1.1" + ), + mock.call( + channel=self._poller_channel_mock, + durable=self._pika_engine.notification_persistence, + exchange="exchange1", + exchange_type='direct', + queue="topic1.2", + queue_expiration=None, + routing_key="topic1.2" + ), + mock.call( + channel=self._poller_channel_mock, + durable=self._pika_engine.notification_persistence, + exchange="exchange2", + exchange_type='direct', + queue="topic2.1", + queue_expiration=None, + routing_key="topic2.1" + ) + )) + + @mock.patch("oslo_messaging._drivers.pika_driver.pika_message." + "PikaIncomingMessage") + def test_declare_notification_queue_bindings_custom_queue( + self, pika_incoming_message_mock): + poller = pika_poller.NotificationPikaPoller( + self._pika_engine, self._target_and_priorities, + "custom_queue_name", self._prefetch_count + ) + self._poller_connection_mock.process_data_events.side_effect = ( + lambda time_limit: poller._on_message_with_ack_callback( + None, None, None, None + ) + ) + + poller.start() + res = poller.poll() + + self.assertEqual(len(res), 1) + + self.assertEqual(res[0], pika_incoming_message_mock.return_value) + + self.assertTrue(self._pika_engine.create_connection.called) + self.assertTrue(self._poller_connection_mock.channel.called) + + declare_queue_binding_by_channel_mock = ( + self._pika_engine.declare_queue_binding_by_channel + ) + + self.assertEqual( + declare_queue_binding_by_channel_mock.call_count, 3 + ) + + declare_queue_binding_by_channel_mock.assert_has_calls(( + mock.call( + channel=self._poller_channel_mock, + durable=self._pika_engine.notification_persistence, + exchange="exchange1", + exchange_type='direct', + queue="custom_queue_name", + queue_expiration=None, + routing_key="topic1.1" + ), + mock.call( + channel=self._poller_channel_mock, + durable=self._pika_engine.notification_persistence, + exchange="exchange1", + exchange_type='direct', + queue="custom_queue_name", + queue_expiration=None, + routing_key="topic1.2" + ), + mock.call( + channel=self._poller_channel_mock, + durable=self._pika_engine.notification_persistence, + exchange="exchange2", + exchange_type='direct', + queue="custom_queue_name", + queue_expiration=None, + routing_key="topic2.1" + ) + ))