PageRenderTime 67ms CodeModel.GetById 39ms app.highlight 23ms RepoModel.GetById 1ms app.codeStats 0ms

/carrot/backends/pyamqplib.py

https://bitbucket.org/invenshure/carrot
Python | 364 lines | 300 code | 33 blank | 31 comment | 27 complexity | 38a8ffb90d69f6ee2e4f64ae19299d40 MD5 | raw file
  1"""
  2
  3`amqplib`_ backend for carrot.
  4
  5.. _`amqplib`: http://barryp.org/software/py-amqplib/
  6
  7"""
  8from amqplib.client_0_8 import transport
  9# amqplib's handshake mistakenly identifies as protocol version 1191,
 10# this breaks in RabbitMQ tip, which no longer falls back to
 11# 0-8 for unknown ids.
 12transport.AMQP_PROTOCOL_HEADER = "AMQP\x01\x01\x08\x00"
 13
 14from amqplib import client_0_8 as amqp
 15from amqplib.client_0_8.exceptions import AMQPConnectionException
 16from amqplib.client_0_8.exceptions import AMQPChannelException
 17from amqplib.client_0_8.serialization import AMQPReader, AMQPWriter
 18from carrot.backends.base import BaseMessage, BaseBackend
 19from itertools import count
 20
 21import socket
 22import warnings
 23import weakref
 24
 25DEFAULT_PORT = 5672
 26
 27
 28class Connection(amqp.Connection):
 29
 30    def drain_events(self, allowed_methods=None, timeout=None):
 31        """Wait for an event on any channel."""
 32        return self.wait_multi(self.channels.values(), timeout=timeout)
 33
 34    def wait_multi(self, channels, allowed_methods=None, timeout=None):
 35        """Wait for an event on a channel."""
 36        chanmap = dict((chan.channel_id, chan) for chan in channels)
 37        chanid, method_sig, args, content = self._wait_multiple(
 38                chanmap.keys(), allowed_methods, timeout=timeout)
 39
 40        channel = chanmap[chanid]
 41
 42        if content \
 43        and channel.auto_decode \
 44        and hasattr(content, 'content_encoding'):
 45            try:
 46                content.body = content.body.decode(content.content_encoding)
 47            except Exception:
 48                pass
 49
 50        amqp_method = channel._METHOD_MAP.get(method_sig, None)
 51
 52        if amqp_method is None:
 53            raise Exception('Unknown AMQP method (%d, %d)' % method_sig)
 54
 55        if content is None:
 56            return amqp_method(channel, args)
 57        else:
 58            return amqp_method(channel, args, content)
 59
 60    def read_timeout(self, timeout=None):
 61        if timeout is None:
 62            return self.method_reader.read_method()
 63        sock = self.transport.sock
 64        prev = sock.gettimeout()
 65        sock.settimeout(timeout)
 66        try:
 67            return self.method_reader.read_method()
 68        finally:
 69            sock.settimeout(prev)
 70
 71    def _wait_multiple(self, channel_ids, allowed_methods, timeout=None):
 72        for channel_id in channel_ids:
 73            method_queue = self.channels[channel_id].method_queue
 74            for queued_method in method_queue:
 75                method_sig = queued_method[0]
 76                if (allowed_methods is None) \
 77                or (method_sig in allowed_methods) \
 78                or (method_sig == (20, 40)):
 79                    method_queue.remove(queued_method)
 80                    method_sig, args, content = queued_method
 81                    return channel_id, method_sig, args, content
 82
 83        # Nothing queued, need to wait for a method from the peer
 84        while True:
 85            channel, method_sig, args, content = self.read_timeout(timeout)
 86
 87            if (channel in channel_ids) \
 88            and ((allowed_methods is None) \
 89                or (method_sig in allowed_methods) \
 90                or (method_sig == (20, 40))):
 91                return channel, method_sig, args, content
 92
 93            # Not the channel and/or method we were looking for. Queue
 94            # this method for later
 95            self.channels[channel].method_queue.append((method_sig,
 96                                                        args,
 97                                                        content))
 98
 99            #
100            # If we just queued up a method for channel 0 (the Connection
101            # itself) it's probably a close method in reaction to some
102            # error, so deal with it right away.
103            #
104            if channel == 0:
105                self.wait()
106
107
108class QueueAlreadyExistsWarning(UserWarning):
109    """A queue with that name already exists, so a recently changed
110    ``routing_key`` or other settings might be ignored unless you
111    rename the queue or restart the broker."""
112
113
114class Message(BaseMessage):
115    """A message received by the broker.
116
117    Usually you don't insantiate message objects yourself, but receive
118    them using a :class:`carrot.messaging.Consumer`.
119
120    :param backend: see :attr:`backend`.
121    :param amqp_message: see :attr:`_amqp_message`.
122
123
124    .. attribute:: body
125
126        The message body.
127
128    .. attribute:: delivery_tag
129
130        The message delivery tag, uniquely identifying this message.
131
132    .. attribute:: backend
133
134        The message backend used.
135        A subclass of :class:`carrot.backends.base.BaseBackend`.
136
137    .. attribute:: _amqp_message
138
139        A :class:`amqplib.client_0_8.basic_message.Message` instance.
140        This is a private attribute and should not be accessed by
141        production code.
142
143    """
144
145    def __init__(self, backend, amqp_message, **kwargs):
146        self._amqp_message = amqp_message
147        self.backend = backend
148
149        for attr_name in ("body",
150                          "delivery_tag",
151                          "content_type",
152                          "content_encoding",
153                          "delivery_info"):
154            kwargs[attr_name] = getattr(amqp_message, attr_name, None)
155
156        super(Message, self).__init__(backend, **kwargs)
157
158
159class Backend(BaseBackend):
160    """amqplib backend
161
162    :param connection: see :attr:`connection`.
163
164
165    .. attribute:: connection
166
167    A :class:`carrot.connection.BrokerConnection` instance. An established
168    connection to the broker.
169
170    """
171    default_port = DEFAULT_PORT
172
173    connection_errors = (AMQPConnectionException,
174                         socket.error,
175                         IOError,
176                         OSError)
177    channel_errors = (AMQPChannelException, )
178
179    Message = Message
180
181    def __init__(self, connection, **kwargs):
182        self.connection = connection
183        self.default_port = kwargs.get("default_port", self.default_port)
184        self._channel_ref = None
185
186    @property
187    def _channel(self):
188        return callable(self._channel_ref) and self._channel_ref()
189
190    @property
191    def channel(self):
192        """If no channel exists, a new one is requested."""
193        if not self._channel:
194            connection = self.connection.connection
195            self._channel_ref = weakref.ref(connection.channel())
196        return self._channel
197
198    def establish_connection(self):
199        """Establish connection to the AMQP broker."""
200        conninfo = self.connection
201        if not conninfo.hostname:
202            raise KeyError("Missing hostname for AMQP connection.")
203        if conninfo.userid is None:
204            raise KeyError("Missing user id for AMQP connection.")
205        if conninfo.password is None:
206            raise KeyError("Missing password for AMQP connection.")
207        if not conninfo.port:
208            conninfo.port = self.default_port
209        return Connection(host=conninfo.host,
210                          userid=conninfo.userid,
211                          password=conninfo.password,
212                          virtual_host=conninfo.virtual_host,
213                          insist=conninfo.insist,
214                          ssl=conninfo.ssl,
215                          connect_timeout=conninfo.connect_timeout)
216
217    def close_connection(self, connection):
218        """Close the AMQP broker connection."""
219        connection.close()
220
221    def queue_exists(self, queue):
222        """Check if a queue has been declared.
223
224        :rtype bool:
225
226        """
227        try:
228            self.channel.queue_declare(queue=queue, passive=True)
229        except AMQPChannelException, e:
230            if e.amqp_reply_code == 404:
231                return False
232            raise e
233        else:
234            return True
235
236    def queue_delete(self, queue, if_unused=False, if_empty=False):
237        """Delete queue by name."""
238        return self.channel.queue_delete(queue, if_unused, if_empty)
239
240    def queue_purge(self, queue, **kwargs):
241        """Discard all messages in the queue. This will delete the messages
242        and results in an empty queue."""
243        return self.channel.queue_purge(queue=queue)
244
245    def queue_declare(self, queue, durable, exclusive, auto_delete,
246            warn_if_exists=False, arguments=None):
247        """Declare a named queue."""
248        if warn_if_exists and self.queue_exists(queue):
249            warnings.warn(QueueAlreadyExistsWarning(
250                QueueAlreadyExistsWarning.__doc__))
251
252        return self.channel.queue_declare(queue=queue,
253                                          durable=durable,
254                                          exclusive=exclusive,
255                                          auto_delete=auto_delete,
256                                          arguments=arguments)
257
258    def exchange_declare(self, exchange, type, durable, auto_delete):
259        """Declare an named exchange."""
260        return self.channel.exchange_declare(exchange=exchange,
261                                             type=type,
262                                             durable=durable,
263                                             auto_delete=auto_delete)
264
265    def queue_bind(self, queue, exchange, routing_key, arguments=None):
266        """Bind queue to an exchange using a routing key."""
267        return self.channel.queue_bind(queue=queue,
268                                       exchange=exchange,
269                                       routing_key=routing_key,
270                                       arguments=arguments)
271
272    def message_to_python(self, raw_message):
273        """Convert encoded message body back to a Python value."""
274        return self.Message(backend=self, amqp_message=raw_message)
275
276    def get(self, queue, no_ack=False):
277        """Receive a message from a declared queue by name.
278
279        :returns: A :class:`Message` object if a message was received,
280            ``None`` otherwise. If ``None`` was returned, it probably means
281            there was no messages waiting on the queue.
282
283        """
284        raw_message = self.channel.basic_get(queue, no_ack=no_ack)
285        if not raw_message:
286            return None
287        return self.message_to_python(raw_message)
288
289    def declare_consumer(self, queue, no_ack, callback, consumer_tag,
290            nowait=False):
291        """Declare a consumer."""
292        return self.channel.basic_consume(queue=queue,
293                                          no_ack=no_ack,
294                                          callback=callback,
295                                          consumer_tag=consumer_tag,
296                                          nowait=nowait)
297
298    def consume(self, limit=None):
299        """Returns an iterator that waits for one message at a time."""
300        for total_message_count in count():
301            if limit and total_message_count >= limit:
302                raise StopIteration
303
304            if not self.channel.is_open:
305                raise StopIteration
306
307            self.channel.wait()
308            yield True
309
310    def cancel(self, consumer_tag):
311        """Cancel a channel by consumer tag."""
312        if not self.channel.connection:
313            return
314        self.channel.basic_cancel(consumer_tag)
315
316    def close(self):
317        """Close the channel if open."""
318        if self._channel and self._channel.is_open:
319            self._channel.close()
320        self._channel_ref = None
321
322    def ack(self, delivery_tag):
323        """Acknowledge a message by delivery tag."""
324        return self.channel.basic_ack(delivery_tag)
325
326    def reject(self, delivery_tag):
327        """Reject a message by deliver tag."""
328        return self.channel.basic_reject(delivery_tag, requeue=False)
329
330    def requeue(self, delivery_tag):
331        """Reject and requeue a message by delivery tag."""
332        return self.channel.basic_reject(delivery_tag, requeue=True)
333
334    def prepare_message(self, message_data, delivery_mode, priority=None,
335                content_type=None, content_encoding=None):
336        """Encapsulate data into a AMQP message."""
337        message = amqp.Message(message_data, priority=priority,
338                               content_type=content_type,
339                               content_encoding=content_encoding)
340        message.properties["delivery_mode"] = delivery_mode
341        return message
342
343    def publish(self, message, exchange, routing_key, mandatory=None,
344            immediate=None, headers=None):
345        """Publish a message to a named exchange."""
346
347        if headers:
348            message.properties["headers"] = headers
349
350        ret = self.channel.basic_publish(message, exchange=exchange,
351                                         routing_key=routing_key,
352                                         mandatory=mandatory,
353                                         immediate=immediate)
354        if mandatory or immediate:
355            self.close()
356
357    def qos(self, prefetch_size, prefetch_count, apply_global=False):
358        """Request specific Quality of Service."""
359        self.channel.basic_qos(prefetch_size, prefetch_count,
360                                apply_global)
361
362    def flow(self, active):
363        """Enable/disable flow from peer."""
364        self.channel.flow(active)