Document the transport backend driver interface

Add detailed documentation to the driver API to help driver developers
create drivers that behave consistently.  Specifically prescribes a
set of operational characteristics that a driver must conform to in
order to provide consistent behavior across different implementations.

Change-Id: Icb251ee724f9a0ac4fede702a367910de4ba95e3
This commit is contained in:
Kenneth Giusti 2016-11-07 15:57:45 -05:00
parent 3f393e521c
commit adf4cd9d05
3 changed files with 445 additions and 60 deletions

View File

@ -0,0 +1,75 @@
---------------------------------------
Guide for Transport Driver Implementors
---------------------------------------
.. currentmodule:: oslo_messaging
.. automodule:: oslo_messaging._drivers.base
============
Introduction
============
This document is a *best practices* guide for the developer interested
in creating a new transport driver for Oslo.Messaging. It should also
be used by maintainers as a reference for proper driver behavior.
This document will describe the driver interface and prescribe the
expected behavior of any driver implemented to this interface.
**Note well:** The API described in this document is internal to the
oslo.messaging library and therefore **private**. Under no
circumstances should this API be referenced by code external to the
oslo.messaging library.
================
Driver Interface
================
The driver interface is defined by a set of abstract base classes. The
developer creates a driver by defining concrete classes from these
bases. The derived classes embody the logic that is specific for the
messaging back-end that is to be supported.
These base classes are defined in the *base.py* file in the *_drivers*
subdirectory.
===============
IncomingMessage
===============
.. autoclass:: IncomingMessage
:members:
==================
RpcIncomingMessage
==================
.. autoclass:: RpcIncomingMessage
:members:
========
Listener
========
.. autoclass:: Listener
:members:
=================
PollStyleListener
=================
.. autoclass:: PollStyleListener
:members:
==========
BaseDriver
==========
.. autoclass:: BaseDriver
:members:

View File

@ -27,6 +27,7 @@ Contents
AMQP1.0 AMQP1.0
pika_driver pika_driver
zmq_driver zmq_driver
driver-dev-guide
FAQ FAQ
contributing contributing

View File

@ -1,4 +1,3 @@
# Copyright 2013 Red Hat, Inc. # Copyright 2013 Red Hat, Inc.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # Licensed under the Apache License, Version 2.0 (the "License"); you may
@ -38,8 +37,9 @@ base_opts = [
def batch_poll_helper(func): def batch_poll_helper(func):
"""Decorator to poll messages in batch """Decorator to poll messages in batch
This decorator helps driver that polls message one by one, This decorator is used to add message batching support to a
to returns a list of message. :py:meth:`PollStyleListener.poll` implementation that only polls for a
single message per call.
""" """
def wrapper(in_self, timeout=None, batch_size=1, batch_timeout=None): def wrapper(in_self, timeout=None, batch_size=1, batch_timeout=None):
incomings = [] incomings = []
@ -78,70 +78,173 @@ class TransportDriverError(exceptions.MessagingException):
@six.add_metaclass(abc.ABCMeta) @six.add_metaclass(abc.ABCMeta)
class IncomingMessage(object): class IncomingMessage(object):
"""The IncomingMessage class represents a single message received from the
messaging backend. Instances of this class are passed to up a server's
messaging processing logic. The backend driver must provide a concrete
derivation of this class which provides the backend specific logic for its
public methods.
:param ctxt: Context metadata provided by sending application.
:type ctxt: dict
:param message: The message as provided by the sending application.
:type message: dict
"""
def __init__(self, ctxt, message): def __init__(self, ctxt, message):
self.ctxt = ctxt self.ctxt = ctxt
self.message = message self.message = message
def acknowledge(self): def acknowledge(self):
"""Acknowledge the message.""" """Called by the server to acknowledge receipt of the message. When
this is called the driver must notify the backend of the
acknowledgment. This call should block at least until the driver has
processed the acknowledgment request locally. It may unblock before the
acknowledgment state has been acted upon by the backend.
If the acknowledge operation fails this method must issue a log message
describing the reason for the failure.
:raises: Does not raise an exception
"""
@abc.abstractmethod @abc.abstractmethod
def requeue(self): def requeue(self):
"""Requeue the message.""" """Called by the server to return the message to the backend so it may
be made available for consumption by another server. This call should
block at least until the driver has processed the requeue request
locally. It may unblock before the backend makes the requeued message
available for consumption.
If the requeue operation fails this method must issue a log message
describing the reason for the failure.
Support for this method is _optional_. The
:py:meth:`BaseDriver.require_features` method should indicate whether
or not support for requeue is available.
:raises: Does not raise an exception
"""
@six.add_metaclass(abc.ABCMeta) @six.add_metaclass(abc.ABCMeta)
class RpcIncomingMessage(IncomingMessage): class RpcIncomingMessage(IncomingMessage):
"""The RpcIncomingMessage represents an RPC request message received from
the backend. This class must be used for RPC calls that return a value to
the caller.
"""
@abc.abstractmethod @abc.abstractmethod
def reply(self, reply=None, failure=None): def reply(self, reply=None, failure=None):
"""Send a reply or failure back to the client.""" """Called by the server to send an RPC reply message or an exception
back to the calling client.
If an exception is passed via *failure* the driver must convert it to
a form that can be sent as a message and properly converted back to the
exception at the remote.
The driver must provide a way to determine the destination address for
the reply. For example the driver may use the *reply-to* field from the
corresponding incoming message. Often a driver will also need to set a
correlation identifier in the reply to help the remote route the reply
to the correct RPCClient.
The driver should provide an *at-most-once* delivery guarantee for
reply messages. This call should block at least until the reply message
has been handed off to the backend - there is no need to confirm that
the reply has been delivered.
If the reply operation fails this method must issue a log message
describing the reason for the failure.
See :py:meth:`BaseDriver.send` for details regarding how the received
reply is processed.
:param reply: reply message body
:type reply: dict
:param failure: an exception thrown by the RPC call
:type failure: Exception
:raises: Does not raise an exception
"""
@six.add_metaclass(abc.ABCMeta) @six.add_metaclass(abc.ABCMeta)
class PollStyleListener(object): class PollStyleListener(object):
"""A PollStyleListener is used to transfer received messages to a server
for processing. A polling pattern is used to retrieve messages. A
PollStyleListener uses a separate thread to run the polling loop. A
:py:class:`PollStyleListenerAdapter` can be used to create a
:py:class:`Listener` from a PollStyleListener.
:param prefetch_size: The number of messages that should be pulled from the
backend per receive transaction. May not be honored by all backend
implementations.
:type prefetch_size: int
"""
def __init__(self, prefetch_size=-1): def __init__(self, prefetch_size=-1):
self.prefetch_size = prefetch_size self.prefetch_size = prefetch_size
@abc.abstractmethod @abc.abstractmethod
def poll(self, timeout=None, batch_size=1, batch_timeout=None): def poll(self, timeout=None, batch_size=1, batch_timeout=None):
"""Blocking until 'batch_size' message is pending and return """poll is called by the server to retrieve incoming messages. It
[IncomingMessage]. blocks until 'batch_size' incoming messages are available, a timeout
Waits for first message. Then waits for next batch_size-1 messages occurs, or the poll is interrupted by a call to the :py:meth:`stop`
during batch window defined by batch_timeout method.
This method block current thread until message comes, stop() is
executed by another thread or timemout is elapsed. If 'batch_size' is > 1 poll must block until 'batch_size' messages are
available or at least one message is available and batch_timeout
expires
:param timeout: Block up to 'timeout' seconds waiting for a message
:type timeout: float
:param batch_size: Block until this number of messages are received.
:type batch_size: int
:param batch_timeout: Time to wait in seconds for a full batch to
arrive. A timer is started when the first message in a batch is
received. If a full batch's worth of messages is not received when
the timer expires then :py:meth:`poll` returns all messages
received thus far.
:type batch_timeout: float
:raises: Does not raise an exception.
:return: A list of up to batch_size IncomingMessage objects.
""" """
def stop(self): def stop(self):
"""Stop listener. """Stop the listener from polling for messages. This method must cause
Stop the listener message polling the :py:meth:`poll` call to unblock and return whatever messages are
currently available. This method is called from a different thread
than the poller so it must be thread-safe.
""" """
pass pass
def cleanup(self): def cleanup(self):
"""Cleanup listener. """Cleanup all resources held by the listener. This method should block
Close connection (socket) used by listener if any. until the cleanup is completed.
As this is listener specific method, overwrite it in to derived class
if cleanup of listener required.
""" """
pass pass
@six.add_metaclass(abc.ABCMeta) @six.add_metaclass(abc.ABCMeta)
class Listener(object): class Listener(object):
"""A Listener is used to transfer incoming messages from the driver to a
server for processing. A callback is used by the driver to transfer the
messages.
:param batch_size: desired number of messages passed to
single on_incoming_callback notification
:type batch_size: int
:param batch_timeout: defines how long should we wait in seconds for
batch_size messages if we already have some messages waiting for
processing
:type batch_timeout: float
:param prefetch_size: defines how many messages we want to prefetch
from the messaging backend in a single request. May not be honored by
all backend implementations.
:type prefetch_size: int
"""
def __init__(self, batch_size, batch_timeout, def __init__(self, batch_size, batch_timeout,
prefetch_size=-1): prefetch_size=-1):
"""Init Listener
:param batch_size: desired number of messages passed to
single on_incoming_callback notification
:param batch_timeout: defines how long should we wait for batch_size
messages if we already have some messages waiting for processing
:param prefetch_size: defines how many massages we want to prefetch
from backend (depend on driver type) by single request
"""
self.on_incoming_callback = None self.on_incoming_callback = None
self.batch_timeout = batch_timeout self.batch_timeout = batch_timeout
self.prefetch_size = prefetch_size self.prefetch_size = prefetch_size
@ -150,31 +253,34 @@ class Listener(object):
self.batch_size = batch_size self.batch_size = batch_size
def start(self, on_incoming_callback): def start(self, on_incoming_callback):
"""Start listener. """Start receiving messages. This should cause the driver to start
Start the listener message polling receiving messages from the backend. When message(s) arrive the driver
must invoke 'on_incoming_callback' passing it the received messages as
a list of IncomingMessages.
:param on_incoming_callback: callback function to be executed when :param on_incoming_callback: callback function to be executed when
listener received messages. Messages should be processed and listener receives messages.
acked/nacked by callback :type on_incoming_callback: func
""" """
self.on_incoming_callback = on_incoming_callback self.on_incoming_callback = on_incoming_callback
def stop(self): def stop(self):
"""Stop listener. """Stop receiving messages. The driver must no longer invoke
Stop the listener message polling the callback.
""" """
self.on_incoming_callback = None self.on_incoming_callback = None
@abc.abstractmethod @abc.abstractmethod
def cleanup(self): def cleanup(self):
"""Cleanup listener. """Cleanup all resources held by the listener. This method should block
Close connection (socket) used by listener if any. until the cleanup is completed.
As this is listener specific method, overwrite it in to derived class
if cleanup of listener required.
""" """
class PollStyleListenerAdapter(Listener): class PollStyleListenerAdapter(Listener):
"""A Listener that uses a PollStyleListener for message transfer. A
dedicated thread is created to do message polling.
"""
def __init__(self, poll_style_listener, batch_size, batch_timeout): def __init__(self, poll_style_listener, batch_size, batch_timeout):
super(PollStyleListenerAdapter, self).__init__( super(PollStyleListenerAdapter, self).__init__(
batch_size, batch_timeout, poll_style_listener.prefetch_size batch_size, batch_timeout, poll_style_listener.prefetch_size
@ -185,13 +291,6 @@ class PollStyleListenerAdapter(Listener):
self._started = False self._started = False
def start(self, on_incoming_callback): def start(self, on_incoming_callback):
"""Start listener.
Start the listener message polling
:param on_incoming_callback: callback function to be executed when
listener received messages. Messages should be processed and
acked/nacked by callback
"""
super(PollStyleListenerAdapter, self).start(on_incoming_callback) super(PollStyleListenerAdapter, self).start(on_incoming_callback)
self._started = True self._started = True
self._listen_thread.start() self._listen_thread.start()
@ -216,25 +315,33 @@ class PollStyleListenerAdapter(Listener):
self.on_incoming_callback(incoming) self.on_incoming_callback(incoming)
def stop(self): def stop(self):
"""Stop listener.
Stop the listener message polling
"""
self._started = False self._started = False
self._poll_style_listener.stop() self._poll_style_listener.stop()
self._listen_thread.join() self._listen_thread.join()
super(PollStyleListenerAdapter, self).stop() super(PollStyleListenerAdapter, self).stop()
def cleanup(self): def cleanup(self):
"""Cleanup listener.
Close connection (socket) used by listener if any.
As this is listener specific method, overwrite it in to derived class
if cleanup of listener required.
"""
self._poll_style_listener.cleanup() self._poll_style_listener.cleanup()
@six.add_metaclass(abc.ABCMeta) @six.add_metaclass(abc.ABCMeta)
class BaseDriver(object): class BaseDriver(object):
"""Defines the backend driver interface. Each backend driver implementation
must provide a concrete derivation of this class implementing the backend
specific logic for its public methods.
:param conf: The configuration settings provided by the user.
:type conf: ConfigOpts
:param url: The network address of the messaging backend(s).
:type url: TransportURL
:param default_exchange: The exchange to use if no exchange is specified in
a Target.
:type default_exchange: str
:param allowed_remote_exmods: whitelist of those exception modules which
are permitted to be re-raised if an exception is returned in response
to an RPC call.
:type allowed_remote_exmods: list
"""
prefetch_size = 0 prefetch_size = 0
def __init__(self, conf, url, def __init__(self, conf, url,
@ -245,30 +352,232 @@ class BaseDriver(object):
self._allowed_remote_exmods = allowed_remote_exmods or [] self._allowed_remote_exmods = allowed_remote_exmods or []
def require_features(self, requeue=False): def require_features(self, requeue=False):
"""The driver must raise a 'NotImplementedError' if any of the feature
flags passed as True are not supported.
"""
if requeue: if requeue:
raise NotImplementedError('Message requeueing not supported by ' raise NotImplementedError('Message requeueing not supported by '
'this transport driver') 'this transport driver')
@abc.abstractmethod @abc.abstractmethod
def send(self, target, ctxt, message, def send(self, target, ctxt, message,
wait_for_reply=None, timeout=None, envelope=False): wait_for_reply=None, timeout=None, envelope=False, retry=None):
"""Send a message to the given target.""" """Send a message to the given target and optionally wait for a reply.
This method is used by the RPC client when sending RPC requests to a
server.
The driver must use the *topic*, *exchange*, and *server* (if present)
attributes of the *target* to construct the backend-native message
address. The message address must match the format used by
subscription(s) created by the :py:meth:`BaseDriver.listen` method.
If the *target's* *fanout* attribute is set, a copy of the message must
be sent to all subscriptions using the *exchange* and *topic*
values. If *fanout* is not set, then only one subscriber should receive
the message. In the case of multiple subscribers to the same address,
only one copy of the message is delivered. In this case the driver
should implement a delivery pattern that distributes messages in a
balanced fashion across the multiple subscribers.
This method must block the caller until one of the following events
occur:
* the send operation completes successfully
* *timeout* seconds elapse (if specified)
* *retry* count is reached (if specified)
The *wait_for_reply* parameter determines whether or not the caller
expects a response to the RPC request. If True, this method must block
until a response message is received. This method then returns the
response message to the caller. The driver must implement a mechanism
for routing incoming responses back to their corresponding send
request. How this is done may vary based on the type of messaging
backend, but typically it involves having the driver create an internal
subscription for reply messages and setting the request message's
*reply-to* header to the subscription address. The driver may also
need to supply a correlation identifier for mapping the response back
to the sender. See :py:meth:`RpcIncomingMessage.reply`
If *wait_for_reply* is False this method will block until the message
has been handed off to the backend - there is no need to confirm that
the message has been delivered. Once the handoff completes this method
returns.
The driver may attempt to retry sending the message should a
recoverable error occur that prevents the message from being passed to
the backend. The *retry* parameter specifies how many attempts to
re-send the message the driver may make before raising a
:py:exc:`MessageDeliveryFailure` exception. A value of None or -1 means
unlimited retries. 0 means no retry is attempted. N means attempt at
most N retries before failing. **Note well:** the driver MUST guarantee
that the message is not duplicated by the retry process.
:param target: The message's destination address
:type target: Target
:param ctxt: Context metadata provided by sending application which
must transfered along with the message.
:type ctxt: dict
:param message: message provided by the caller
:type message: dict
:param wait_for_reply: If True block until a reply message is received.
:type wait_for_reply: bool
:param timeout: Maximum time in seconds to block waiting for the send
operation to complete. Should this expire the :py:meth:`send` must
raise a :py:exc:`MessagingTimeout` exception
:type timeout: float
:param retry: maximum message send attempts permitted
:type retry: int
:returns: A reply message or None if no reply expected
:raises: :py:exc:`MessagingException`, any exception thrown by the
remote server when executing the RPC call.
"""
@abc.abstractmethod @abc.abstractmethod
def send_notification(self, target, ctxt, message, version): def send_notification(self, target, ctxt, message, version, retry):
"""Send a notification message to the given target.""" """Send a notification message to the given target. This method is used
by the Notifier to send notification messages to a Listener.
Notifications use a *store and forward* delivery pattern. The driver
must allow for delivery in the case where the intended recipient is
not present at the time the notification is published. Typically this
requires a messaging backend that has the ability to store messages
until a consumer is present.
Therefore this method must block at least until the backend accepts
ownership of the message. This method does not guarantee that the
message has or will be processed by the intended recipient.
The driver must use the *topic* and *exchange* attributes of the
*target* to construct the backend-native message address. The message
address must match the format used by subscription(s) created by the
:py:meth:`BaseDriver.listen_for_notifications` method. Only one copy of
the message is delivered in the case of multiple subscribers to the
same address. In this case the driver should implement a delivery
pattern that distributes messages in a balanced fashion across the
multiple subscribers.
There is an exception to the single delivery semantics described above:
the *pool* parameter to the
:py:meth:`BaseDriver.listen_for_notifications` method may be used to
set up shared subscriptions. See
:py:meth:`BaseDriver.listen_for_notifications` for details.
This method must also honor the *retry* parameter. See
:py:meth:`BaseDriver.send` for details regarding implementing the
*retry* process.
*version* indicates whether or not the message should be encapsulated
in an envelope. A value < 2.0 should not envelope the message. See
:py:func:`common.serialize_msg` for more detail.
:param target: The message's destination address
:type target: Target
:param ctxt: Context metadata provided by sending application which
must transfered along with the message.
:type ctxt: dict
:param message: message provided by the caller
:type message: dict
:param version: determines the envelope for the message
:type version: float
:param retry: maximum message send attempts permitted
:type retry: int
:returns: None
:raises: :py:exc:`MessagingException`
"""
@abc.abstractmethod @abc.abstractmethod
def listen(self, target, batch_size, batch_timeout): def listen(self, target, batch_size, batch_timeout):
"""Construct a Listener for the given target.""" """Construct a listener for the given target. The listener may be
either a :py:class:`Listener` or :py:class:`PollStyleListener`
depending on the driver's preference. This method is used by the RPC
server.
The driver must create subscriptions to the address provided in
*target*. These subscriptions must then be associated with a
:py:class:`Listener` or :py:class:`PollStyleListener` which is returned
by this method. See :py:meth:`BaseDriver.send` for more detail
regarding message addressing.
The driver must support receiving messages sent to the following
addresses derived from the values in *target*:
* all messages sent to the exchange and topic given in the target.
This includes messages sent using a fanout pattern.
* if the server attribute of the target is set then the driver must
also subscribe to messages sent to the exchange, topic, and server
For example, given a target with exchange 'my-exchange', topic
'my-topic', and server 'my-server', the driver would create
subscriptions for:
* all messages sent to my-exchange and my-topic (including fanout)
* all messages sent to my-exchange, my-topic, and my-server
The driver must pass messages arriving from these subscriptions to the
listener. For :py:class:`PollStyleListener` the driver should trigger
the :py:meth:`PollStyleListener.poll` method to unblock and return the
incoming messages. For :py:class:`Listener` the driver should invoke
the callback with the incoming messages.
This method only blocks long enough to establish the subscription(s)
and construct the listener. In the case of failover, the driver must
restore the subscription(s). Subscriptions should remain active until
the listener is stopped.
:param target: The address(es) to subscribe to.
:type target: Target
:param batch_size: passed to the listener
:type batch_size: int
:param batch_timeout: passed to the listener
:type batch_timeout: float
:returns: None
:raises: :py:exc:`MessagingException`
"""
@abc.abstractmethod @abc.abstractmethod
def listen_for_notifications(self, targets_and_priorities, pool, def listen_for_notifications(self, targets_and_priorities, pool,
batch_size, batch_timeout): batch_size, batch_timeout):
"""Construct a notification Listener for the given list of """Construct a notification listener for the given list of
tuple of (target, priority). tuples of (target, priority) addresses.
The driver must create a subscription for each (*target*, *priority*)
pair. The topic for the subscription is created for each pair using the
format `"%s.%s" % (target.topic, priority)`. This format is used by
the caller of the :py:meth:`BaseDriver.send_notification` when setting
the topic member of the target parameter.
Only the *exchange* and *topic* must be considered when creating
subscriptions. *server* and *fanout* must be ignored.
The *pool* parameter, if specified, should cause the driver to create a
subscription that is shared with other subscribers using the same pool
identifier. Each pool gets a single copy of the message. For example if
there is a subscriber pool with identifier **foo** and another pool
**bar**, then one **foo** subscriber and one **bar** subscriber will
each receive a copy of the message. The driver should implement a
delivery pattern that distributes message in a balanced fashion across
the subscribers in a pool.
The driver must raise a :py:exc:`NotImplementedError` if pooling is not
supported and a pool identifier is passed in.
Refer to the description of :py:meth:`BaseDriver.send_notification` for
further details regarding implementation.
:param targets_and_priorities: List of (target, priority) pairs
:type targets_and_priorities: list
:param pool: pool identifier
:type pool: str
:param batch_size: passed to the listener
:type batch_size: int
:param batch_timeout: passed to the listener
:type batch_timeout: float
:returns: None
:raises: :py:exc:`MessagingException`, :py:exc:`NotImplementedError`
""" """
@abc.abstractmethod @abc.abstractmethod
def cleanup(self): def cleanup(self):
"""Release all resources.""" """Release all resources used by the driver. This method must block
until the cleanup is complete.
"""