50b5468109
1) Typos in log messages 2) Change LOG.warn -> LOG.warning 3) Fix names of varibles 4) Stop logic of RPC reply listener fixed Change-Id: Icf4579f25f32f65428a3e7e6383bf8b1e34a6fa6
461 lines
18 KiB
Python
461 lines
18 KiB
Python
# 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 threading
|
|
import time
|
|
|
|
from oslo_log import log as logging
|
|
from oslo_utils import timeutils
|
|
import six
|
|
|
|
from oslo_messaging._drivers import base
|
|
from oslo_messaging._drivers.pika_driver import pika_commons as pika_drv_cmns
|
|
from oslo_messaging._drivers.pika_driver import pika_exceptions as pika_drv_exc
|
|
from oslo_messaging._drivers.pika_driver import pika_message as pika_drv_msg
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
class PikaPoller(base.Listener):
|
|
"""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, incoming_message_class):
|
|
"""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._started = False
|
|
|
|
self._queues_to_consume = None
|
|
|
|
self._message_queue = []
|
|
|
|
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
|
|
)
|
|
self._channel = self._connection.channel()
|
|
self._channel.basic_qos(prefetch_count=self._prefetch_count)
|
|
|
|
if self._queues_to_consume is None:
|
|
self._queues_to_consume = self._declare_queue_binding()
|
|
|
|
self._start_consuming()
|
|
|
|
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):
|
|
"""Is called by recovering connection logic for starting consumption
|
|
of configured RabbitMQ queues
|
|
"""
|
|
|
|
assert self._queues_to_consume is not None
|
|
|
|
try:
|
|
for queue_info in self._queues_to_consume:
|
|
no_ack = queue_info["no_ack"]
|
|
|
|
on_message_callback = (
|
|
self._on_message_no_ack_callback if no_ack
|
|
else self._on_message_with_ack_callback
|
|
)
|
|
|
|
queue_info["consumer_tag"] = self._channel.basic_consume(
|
|
on_message_callback, queue_info["queue_name"],
|
|
no_ack=no_ack
|
|
)
|
|
except Exception:
|
|
self._queues_to_consume = None
|
|
raise
|
|
|
|
def _stop_consuming(self):
|
|
"""Is called by poller's stop logic for stopping consumption
|
|
of configured RabbitMQ queues
|
|
"""
|
|
|
|
assert self._queues_to_consume is not None
|
|
|
|
for queue_info in self._queues_to_consume:
|
|
consumer_tag = queue_info["consumer_tag"]
|
|
if consumer_tag is not None:
|
|
self._channel.basic_cancel(consumer_tag)
|
|
queue_info["consumer_tag"] = None
|
|
|
|
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._incoming_message_class(
|
|
self._pika_engine, None, method, properties, body
|
|
)
|
|
)
|
|
|
|
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._incoming_message_class(
|
|
self._pika_engine, self._channel, method, properties, body
|
|
)
|
|
)
|
|
|
|
def _cleanup(self):
|
|
"""Cleanup allocated resources (channel, connection, etc). It is unsafe
|
|
method for internal use only
|
|
"""
|
|
if self._connection:
|
|
try:
|
|
self._connection.close()
|
|
except pika_drv_cmns.PIKA_CONNECTIVITY_ERRORS:
|
|
# expected errors
|
|
pass
|
|
except Exception:
|
|
LOG.exception("Unexpected error during closing connection")
|
|
finally:
|
|
self._channel = None
|
|
self._connection = None
|
|
|
|
for i in six.moves.range(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
|
|
|
|
:param: timeout: float, seconds, timeout for waiting new incoming
|
|
message, None means wait forever
|
|
: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
|
|
"""
|
|
|
|
with timeutils.StopWatch(timeout) as stop_watch:
|
|
while True:
|
|
with self._lock:
|
|
last_queue_size = len(self._message_queue)
|
|
|
|
if (last_queue_size >= prefetch_size
|
|
or stop_watch.expired()):
|
|
result = self._message_queue[:prefetch_size]
|
|
del self._message_queue[:prefetch_size]
|
|
return result
|
|
|
|
try:
|
|
if self._started:
|
|
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
|
|
)
|
|
else:
|
|
# consumer is stopped so we don't expect new
|
|
# messages, just process already sent events
|
|
if self._channel is not None:
|
|
self._connection.process_data_events(
|
|
time_limit=0
|
|
)
|
|
# and return result if we don't see new messages
|
|
if last_queue_size == len(self._message_queue):
|
|
result = self._message_queue[:prefetch_size]
|
|
del self._message_queue[:prefetch_size]
|
|
return result
|
|
except pika_drv_exc.EstablishConnectionException as e:
|
|
LOG.warning(
|
|
"Problem during establishing connection for pika "
|
|
"poller %s", e, exc_info=True
|
|
)
|
|
time.sleep(
|
|
self._pika_engine.host_connection_reconnect_delay
|
|
)
|
|
except pika_drv_exc.ConnectionException:
|
|
self._cleanup()
|
|
raise
|
|
except pika_drv_cmns.PIKA_CONNECTIVITY_ERRORS:
|
|
self._cleanup()
|
|
raise
|
|
|
|
def start(self):
|
|
"""Starts poller. Should be called before polling to allow message
|
|
consuming
|
|
"""
|
|
with self._lock:
|
|
if self._started:
|
|
return
|
|
|
|
try:
|
|
self._reconnect()
|
|
except pika_drv_exc.EstablishConnectionException as exc:
|
|
LOG.warning(
|
|
"Can not establish connection during pika poller's "
|
|
"start(). Connecting is required during first poll() "
|
|
"call. %s", exc, exc_info=True
|
|
)
|
|
except pika_drv_exc.ConnectionException as exc:
|
|
self._cleanup()
|
|
LOG.warning(
|
|
"Connectivity problem during pika poller's start(). "
|
|
"Reconnecting required during first poll() call. %s",
|
|
exc, exc_info=True
|
|
)
|
|
except pika_drv_cmns.PIKA_CONNECTIVITY_ERRORS as exc:
|
|
self._cleanup()
|
|
LOG.warning(
|
|
"Connectivity problem during pika poller's start(). "
|
|
"Reconnecting required during first poll() call. %s",
|
|
exc, exc_info=True
|
|
)
|
|
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
|
|
|
|
if self._queues_to_consume and self._channel:
|
|
try:
|
|
self._stop_consuming()
|
|
except pika_drv_cmns.PIKA_CONNECTIVITY_ERRORS as exc:
|
|
self._cleanup()
|
|
LOG.warning(
|
|
"Connectivity problem detected during consumer "
|
|
"cancellation. %s", exc, exc_info=True
|
|
)
|
|
self._started = False
|
|
|
|
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,
|
|
incoming_message_class=pika_drv_msg.RpcPikaIncomingMessage
|
|
)
|
|
|
|
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.rpc_queue_expiration
|
|
|
|
exchange = self._pika_engine.get_rpc_exchange_name(
|
|
self._target.exchange
|
|
)
|
|
|
|
queues_to_consume = []
|
|
|
|
for no_ack in [True, False]:
|
|
queue = self._pika_engine.get_rpc_queue_name(
|
|
self._target.topic, None, 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
|
|
)
|
|
queues_to_consume.append(
|
|
{"queue_name": queue, "no_ack": no_ack, "consumer_tag": None}
|
|
)
|
|
|
|
if self._target.server:
|
|
server_queue = self._pika_engine.get_rpc_queue_name(
|
|
self._target.topic, self._target.server, no_ack
|
|
)
|
|
self._pika_engine.declare_queue_binding_by_channel(
|
|
channel=self._channel, exchange=exchange, durable=False,
|
|
queue=server_queue, routing_key=server_queue,
|
|
exchange_type='direct', queue_expiration=queue_expiration
|
|
)
|
|
queues_to_consume.append(
|
|
{"queue_name": server_queue, "no_ack": no_ack,
|
|
"consumer_tag": None}
|
|
)
|
|
|
|
worker_queue = self._pika_engine.get_rpc_queue_name(
|
|
self._target.topic, self._target.server, no_ack, True
|
|
)
|
|
all_workers_routing_key = self._pika_engine.get_rpc_queue_name(
|
|
self._target.topic, "all_workers", no_ack
|
|
)
|
|
self._pika_engine.declare_queue_binding_by_channel(
|
|
channel=self._channel, exchange=exchange, durable=False,
|
|
queue=worker_queue, routing_key=all_workers_routing_key,
|
|
exchange_type='direct', queue_expiration=queue_expiration
|
|
)
|
|
queues_to_consume.append(
|
|
{"queue_name": worker_queue, "no_ack": no_ack,
|
|
"consumer_tag": None}
|
|
)
|
|
|
|
return queues_to_consume
|
|
|
|
|
|
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
|
|
|
|
super(RpcReplyPikaPoller, self).__init__(
|
|
pika_engine=pika_engine, prefetch_count=prefetch_count,
|
|
incoming_message_class=pika_drv_msg.RpcReplyPikaIncomingMessage
|
|
)
|
|
|
|
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
|
|
"""
|
|
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=self._pika_engine.rpc_queue_expiration,
|
|
durable=False
|
|
)
|
|
|
|
return [{"queue_name": self._queue, "no_ack": False,
|
|
"consumer_tag": None}]
|
|
|
|
|
|
class NotificationPikaPoller(PikaPoller):
|
|
"""PikaPoller implementation for polling Notification messages. Overrides
|
|
base functionality according to Notification specific
|
|
"""
|
|
def __init__(self, pika_engine, targets_and_priorities, prefetch_count,
|
|
queue_name=None):
|
|
"""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
|
|
:param targets_and_priorities: list of (target, priority), defines
|
|
default queue names for corresponding notification types
|
|
:param prefetch_count: Integer, maximum count of unacknowledged
|
|
messages which RabbitMQ broker sends to this consumer
|
|
:param queue: String, alternative queue name used for this poller
|
|
instead of default queue name
|
|
"""
|
|
self._targets_and_priorities = targets_and_priorities
|
|
self._queue_name = queue_name
|
|
|
|
super(NotificationPikaPoller, self).__init__(
|
|
pika_engine, prefetch_count=prefetch_count,
|
|
incoming_message_class=pika_drv_msg.PikaIncomingMessage
|
|
)
|
|
|
|
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)
|
|
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,
|
|
durable=self._pika_engine.notification_persistence,
|
|
)
|
|
queues_to_consume.append(
|
|
{"queue_name": queue, "no_ack": False, "consumer_tag": None}
|
|
)
|
|
|
|
return queues_to_consume
|