diff --git a/.gitignore b/.gitignore index 5c528f964aa7b08cb10b3b0da3d3248894079b25..a9dc4234365eff13880f4cf72e17a8e2634d3d8f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ __pycache__/ # C extensions *.so - +.pycharm_helpers/ # Distribution / packaging .Python env/ @@ -25,6 +25,7 @@ var/ *.egg-info/ .installed.cfg *.egg +*.pdf # PyInstaller # Usually these files are written by a python script from a template diff --git a/LICENSE b/LICENSE index 9d8f6d330a52f91c22a43fb71b6ce3963c69613c..5dfffdb0cdce280eecdca17f31d08c974799c208 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2016 DMS Serwis s.c. +Copyright (c) 2016-2017 DMS Serwis s.c. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -21,4 +21,4 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -resources/amqp-0-9-1.xml: Copyright (c) 2016 OASIS. All rights reserved. \ No newline at end of file +resources/*: consult each file for respective copyright holders \ No newline at end of file diff --git a/README.md b/README.md index e59f2a79ccb7f8890f28da739eb82cc27b07fc28..feb513202082febc2a2c79cbdb181f6c2d3c3d82 100644 --- a/README.md +++ b/README.md @@ -8,33 +8,22 @@ CoolAMQP []() []() -**API WILL CHANGE MASSIVELY IN v1.0!!!! DO NOT USE FOR NOW!!!!** +A **magical** AMQP client, that uses **heavy sorcery** to achieve speeds that other AMQP clients cannot even hope to match. -When you're tired of fucking with AMQP reconnects. +tl;dr - [this](coolamqp/framing/definitions.py) is **machine-generated** compile-time. +[this](coolamqp/framing/compilation/content_property.py) **generates classes run-time**. -When a connection made by CoolAMQP to your broker fails, it will pick another -node, redeclare exchanges, queues, consumers, QoS and all the other shit, and tell -your application that a disconnect happened. - -You only need to remember that: - -1. Reconnects and redefinitions take a while. Things will happen during that time. It is your responsibility to ensure that your distributed system is built to handle this -2. CoolAMQP will tell you when it senses losing broker connection. It will also tell you when it regains the connection (that means that everything is redefined and ready to go) -3. Delivering messages multiple times may happen. Ensure you know when it happens. Keywords: message acknowledgement, amqp specification -4. CoolAMQP won't touch your messages. You send bags of bytes and properties, you get bags of bytes and their properties. This is by design - the postman shouldn't mess with your mail. The project is actively maintained and used in a commercial project. Tests can run either on Vagrant (Vagrantfile attached) or Travis CI, and run against RabbitMQ. Enjoy! -# Changelog -## v0.12 -* ACCESS_REFUSED/RESOURCE_LOCKED on reconnect is properly handled -* reason for consumer cancel is provided -* can read error code and reply text from failed orders -* test suite refactored and improved -## v0.11 -* added *no_ack* to *consume* -* can pass other non-text types to Message -* can set global bit in *qos* + +## Notes +Assertions are sprinkled throughout the code. You may wish to run with optimizations enabled +if you need every CPU cycle you can get. + +**v0.8** series has unstable API. + +**v0.9** series will have a stable API. \ No newline at end of file diff --git a/coolamqp/__init__.py b/coolamqp/__init__.py index 64f889284517fbb688deda2350210370ee5285ff..a8863d8254d08542d55ff98ca2ceddbf69b76511 100644 --- a/coolamqp/__init__.py +++ b/coolamqp/__init__.py @@ -1,6 +1,2 @@ # coding=UTF-8 -from coolamqp.cluster import ClusterNode, Cluster -from coolamqp.events import ConnectionDown, ConnectionUp, MessageReceived, ConsumerCancelled -from coolamqp.messages import Message, Exchange, Queue -from coolamqp.backends.base import Cancelled, Discarded diff --git a/coolamqp/attaches/__init__.py b/coolamqp/attaches/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..5075d303b83e0c24bb739eb6716c7a8c914b0304 --- /dev/null +++ b/coolamqp/attaches/__init__.py @@ -0,0 +1,17 @@ +# coding=UTF-8 +from __future__ import absolute_import, division, print_function +""" +Attaches are components that attach to an coolamqp.uplink.Connection and perform some duties +These duties almost always require allocating a channel. A base class - Channeler - is provided to faciliate that. +The attache becomes then responsible for closing this channel. + +Attache should also register at least one on_fail watch, so it can handle things if they go south. + +Multiple attaches can be "abstracted" as single one via AttacheGroup (which is also an Attache) + +EVERYTHING HERE IS CALLED BY LISTENER THREAD UNLESS STATED OTHERWISE. +""" + +from coolamqp.attaches.consumer import Consumer +from coolamqp.attaches.publisher import Publisher +from coolamqp.attaches.agroup import AttacheGroup diff --git a/coolamqp/attaches/agroup.py b/coolamqp/attaches/agroup.py new file mode 100644 index 0000000000000000000000000000000000000000..2e3f3440b7939ef14788b8fdbfcaad24fe889f2f --- /dev/null +++ b/coolamqp/attaches/agroup.py @@ -0,0 +1,74 @@ +# coding=UTF-8 +""" +This is an attache that attaches multiple attaches. + +It evicts cancelled attaches. +""" +from __future__ import print_function, absolute_import, division +import six +import logging +import weakref + +logger = logging.getLogger(__name__) + + +from coolamqp.attaches.channeler import Attache, ST_OFFLINE +from coolamqp.attaches.consumer import Consumer + + +class AttacheGroup(Attache): + """ + A bunch of attaches + """ + + def __init__(self): + super(AttacheGroup, self).__init__() + self.attaches = [] + + def add(self, attache): + """ + Add an attache to this group. + + If this is attached, and connection is ST_ONLINE, .attach() will be called + on this attache at once. + + :param attache: Attache instance + """ + assert attache not in self.attaches + self.attaches.append(attache) + + # If we have any connection, and it's not dead, attach + if self.connection is not None and self.connection.state != ST_OFFLINE: + attache.attach(self.connection) + + if isinstance(attache, Consumer): + attache.attache_group = self + + def on_cancel_customer(self, customer): + """ + Called by a customer, when it's cancelled. + + Consumer must have .attache_group set to this. This is done by .add() + + :param customer: a Customer instance + """ + self.attaches.remove(customer) + + def attach(self, connection): + """ + Attach to a connection + + :param connection: Connection instance of any state + """ + # since this attache does not watch for failures, it can't use typical method. + self.connection = connection + + for attache in self.attaches: + if not attache.cancelled: + print('Attaching', attache) + attache.attach(connection) + else: + print('lol wut') + raise Exception + + diff --git a/coolamqp/attaches/channeler.py b/coolamqp/attaches/channeler.py new file mode 100644 index 0000000000000000000000000000000000000000..a892b751dac87184c7eeaada419bf44238cd6732 --- /dev/null +++ b/coolamqp/attaches/channeler.py @@ -0,0 +1,207 @@ +# coding=UTF-8 +""" +Base class for consumer or publisher with the capabiility to +set up and tear down channels +""" +from __future__ import print_function, absolute_import, division +import six +from coolamqp.framing.frames import AMQPMethodFrame, AMQPBodyFrame, AMQPHeaderFrame +from coolamqp.framing.definitions import ChannelOpen, ChannelOpenOk, BasicConsume, \ + BasicConsumeOk, QueueDeclare, QueueDeclareOk, ExchangeDeclare, ExchangeDeclareOk, \ + QueueBind, QueueBindOk, ChannelClose, ChannelCloseOk, BasicCancel, BasicDeliver, \ + BasicAck, BasicReject, ACCESS_REFUSED, RESOURCE_LOCKED, BasicCancelOk +from coolamqp.uplink import HeaderOrBodyWatch, MethodWatch +import logging + +ST_OFFLINE = 0 # Consumer is *not* consuming, no setup attempts are being made +ST_SYNCING = 1 # A process targeted at consuming has been started +ST_ONLINE = 2 # Consumer is declared all right + + +logger = logging.getLogger(__name__) + + +class Attache(object): + """ + Something that can be attached to connection. + """ + def __init__(self): + self.cancelled = False #: public, if this is True, it won't be attached to next connection + self.state = ST_OFFLINE + self.connection = None + + def attach(self, connection): + """ + Attach to a connection. + + :param connection: Connection instance of any state + """ + assert self.connection is None + assert connection.state != ST_OFFLINE + self.connection = connection + + +class Channeler(Attache): + """ + A base class for Consumer/Publisher implementing link set up and tear down. + + A channeler can be essentially in 4 states: + - ST_OFFLINE (.channel is None): channel is closed, object is unusable. Requires an attach() a connection + that is being established, or open, or whatever. Connection will notify + this channeler that it's open. + - ST_SYNCING: channeler is opening a channel/doing some other things related to it's setup. + it's going to be ST_ONLINE soon, or go back to ST_OFFLINE. + It has, for sure, acquired a channel number. + - ST_ONLINE: channeler is operational. It has a channel number and has declared everything + it needs to. + + on_operational(True) will be called when a transition is made TO this state. + on_operational(False) will be called when a transition is made FROM this state. + + - ST_OFFLINE (.channel is not None): channeler is undergoing a close. It has not yet torn down the channel, + but ordering it to do anything is pointless, because it will not get done + until attach() with new connection is called. + """ + + def __init__(self): + """ + [EXTEND ME!] + """ + super(Channeler, self).__init__() + self.channel_id = None # channel obtained from Connection + + def attach(self, connection): + """ + Attach this object to a live Connection. + + :param connection: Connection instance to use + """ + super(Channeler, self).attach(connection) + assert self.connection is not None + connection.call_on_connected(self.on_uplink_established) + + # ------- event handlers + + def on_operational(self, operational): + """ + [EXTEND ME] Called by internal methods (on_*) when channel has achieved (or lost) operational status. + + If this is called with operational=True, then for sure it will be called with operational=False. + + This will, therefore, get called an even number of times. + + :param operational: True if channel has just become operational, False if it has just become useless. + """ + + def on_close(self, payload=None): + """ + [EXTEND ME] Handler for channeler destruction. + + Called on: + - channel exception + - connection failing + + This handles following situations: + - payload is None: this means that connection has gone down hard, so our Connection object is + probably very dead. Transition to ST_OFFLINE (.channel is None) + - payload is a ChannelClose: this means that a channel exception has occurred. Dispatch a ChannelCloseOk, + attempt to log an exception, transition to ST_OFFLINE (.channel is None) + - payload is a ChannelCloseOk: this means that it was us who attempted to close the channel. Return the channel + to free pool, transition to ST_OFFLINE (.channel is None) + + If you need to handle something else, extend this. Take care that this DOES NOT HANDLE errors that happen + while state is ST_SYNCING. You can expect this to handle a full channel close, therefore releasing all + resources, so it mostly will do *the right thing*. + + If you need to do something else than just close a channel, please extend or modify as necessary. + + WARNING: THIS WILL GET CALLED TWICE. + Once on ChannelClose - if so, + Second with None - because socket dies. + + Be prepared! + + """ + if self.connection is None: + # teardown already done + return + + if self.state == ST_ONLINE: + # The channel has just lost operationality! + self.on_operational(False) + self.state = ST_OFFLINE + + if isinstance(payload, (ChannelClose, ChannelCloseOk)): + assert self.channel_id is not None + self.connection.free_channels.append(self.channel_id) + # it's just dead don't bother with returning port + + self.connection = None + self.channel_id = None + print(self, 'pwned with', payload) + + if isinstance(payload, ChannelClose): + logger.debug('Channel closed: %s %s', payload.reply_code, payload.reply_text) + + def methods(self, payloads): + """ + Syntactic sugar for + + for payload in paylods: + self.method(payload) + + But moar performant. + """ + assert self.channel_id is not None + frames = [AMQPMethodFrame(self.channel_id, payload) for payload in payloads] + self.connection.send(frames) + + def method(self, payload): + """ + Syntactic sugar for: + + self.connection.send([AMQPMethodFrame(self.channel_id, payload)]) + """ + self.methods([payload]) + + def method_and_watch(self, method_payload, method_classes_to_watch, callable): + """ + Syntactic sugar for + + self.connection.method_and_watch(self.channel_id, + method_payload, + method_classes_to_watch, + callable) + """ + assert self.channel_id is not None + self.connection.method_and_watch(self.channel_id, method_payload, method_classes_to_watch, callable) + + def on_setup(self, payload): + """ + [OVERRIDE ME!] Called with a method frame that signifies a part of setup. + + You must be prepared to handle at least a payload of ChannelOpenOk + + :param payload: AMQP method frame payload + """ + raise Exception('Abstract method - override me!') + + + def on_uplink_established(self): + """Called by connection. Connection reports being ready to do things.""" + assert self.connection is not None + assert self.connection.state == ST_ONLINE, repr(self) + self.state = ST_SYNCING + self.channel_id = self.connection.free_channels.pop() + + self.connection.watch_for_method(self.channel_id, (ChannelClose, ChannelCloseOk, BasicCancel), + self.on_close, + on_fail=self.on_close) + + self.connection.method_and_watch( + self.channel_id, + ChannelOpen(), + ChannelOpenOk, + self.on_setup + ) + diff --git a/coolamqp/attaches/consumer.py b/coolamqp/attaches/consumer.py new file mode 100644 index 0000000000000000000000000000000000000000..bee5f12ed7151017bcb632110380f3b682d60e66 --- /dev/null +++ b/coolamqp/attaches/consumer.py @@ -0,0 +1,405 @@ +# coding=UTF-8 +from __future__ import absolute_import, division, print_function +import six +import logging +from coolamqp.framing.frames import AMQPBodyFrame, AMQPHeaderFrame +from coolamqp.framing.definitions import ChannelOpenOk, BasicConsume, \ + BasicConsumeOk, QueueDeclare, QueueDeclareOk, ExchangeDeclare, ExchangeDeclareOk, \ + QueueBind, QueueBindOk, ChannelClose, BasicCancel, BasicDeliver, \ + BasicAck, BasicReject, RESOURCE_LOCKED, BasicCancelOk, BasicQos, HARD_ERROR +from coolamqp.uplink import HeaderOrBodyWatch, MethodWatch + +from coolamqp.attaches.channeler import Channeler, ST_ONLINE, ST_OFFLINE +from coolamqp.exceptions import ResourceLocked, AMQPError + + +logger = logging.getLogger(__name__) + + +class Consumer(Channeler): + """ + This object represents a consumer in the system. + + Consumer may reside on any AMQP broker, this is to be decided by CoolAMQP. + Consumer, when created, has the state of ST_SYNCING. CoolAMQP will + try to declare the consumer where it makes most sense for it to be. + + If it succeeds, the consumer will enter state ST_ONLINE, and callables + on_start will be called. This means that broker has confirmed that this + consumer is operational and receiving messages. + + Note that does not attempt to cancel consumers, or any of such nonsense. Having + a channel per consumer gives you the unique possibility of simply closing the channel. + Since this implies cancelling the consumer, here you go. + """ + + def __init__(self, queue, on_message, no_ack=True, qos=None, cancel_on_failure=False, + future_to_notify=None, + fail_on_first_time_resource_locked=False + ): + """ + :param queue: Queue object, being consumed from right now. + Note that name of anonymous queue might change at any time! + :param on_message: callable that will process incoming messages + :type on_message: callable(ReceivedMessage instance) + :param no_ack: Will this consumer require acknowledges from messages? + :param qos: a tuple of (prefetch size, prefetch window) for this consumer + :type qos: tuple(int, int) or tuple(None, int) + :param cancel_on_failure: Consumer will cancel itself when link goes down + :type cancel_on_failure: bool + :param future_to_notify: Future to succeed when this consumer goes online for the first time. + This future can also raise with: + AMQPError - a HARD_ERROR (see AMQP spec) was encountered + ResourceLocked - this was the first declaration, and + fail_on_first_time_resource_locked was set + :param fail_on_first_time_resource_locked: When consumer is declared for the first time, + and RESOURCE_LOCKED is encountered, it will fail the + future with ResourceLocked, and consumer will cancel itself. + By default it will retry until success is made. + If the consumer doesn't get the chance to be declared - because + of a connection fail - next reconnect will consider this to be + SECOND declaration, ie. it will retry ad infinitum + :type fail_on_first_time_resource_locked: bool + """ + super(Consumer, self).__init__() + + self.queue = queue + self.no_ack = no_ack + + self.on_message = on_message + + # private + self.cancelled = False # did the client want to STOP using this consumer? + self.receiver = None # MessageReceiver instance + + self.attache_group = None # attache group this belongs to. + # if this is not None, then it has an attribute + # on_cancel_customer(Consumer instance) + if qos is not None: + if qos[0] is None: + qos = 0, qos[1] # prefetch_size=0=undefined + self.qos = qos + self.qos_update_sent = False # QoS was not sent to server + + self.future_to_notify = future_to_notify + self.fail_on_first_time_resource_locked = fail_on_first_time_resource_locked + self.cancel_on_failure = cancel_on_failure + + def set_qos(self, prefetch_size, prefetch_count): + """ + Set new QoS for this consumer. + + :param prefetch_size: prefetch in octets + :param prefetch_count: prefetch in whole messages + """ + if self.state == ST_ONLINE: + self.method(BasicQos(prefetch_size or 0, prefetch_count, False)) + self.qos = prefetch_size or 0, prefetch_count + + def cancel(self): + """ + Cancel the customer. + + Note that this is a departure form AMQP specification. We don't attempt to cancel the customer, + we simply trash the channel. Idk if it's a good idea... + + .ack() or .nack() for messages from this customer will have no effect. + """ + self.cancelled = True + self.method(ChannelClose(0, b'consumer cancelled', 0, 0)) + if self.attache_group is not None: + self.attache_group.on_cancel_customer(self) + + + def on_operational(self, operational): + super(Consumer, self).on_operational(operational) + + if operational: + assert self.receiver is None + self.receiver = MessageReceiver(self) + + # notify the future + if self.future_to_notify is not None: + self.future_to_notify.set_result() + self.future_to_notify = None + + else: + self.receiver.on_gone() + self.receiver = None + + def on_close(self, payload=None): + """ + Handle closing the channel. It sounds like an exception... + + This is done in two steps: + 1. self.state <- ST_OFFLINE, on_event(EV_OFFLINE) upon detecting that no more messages will + be there + 2. self.channel_id <- None, channel is returned to Connection - channel has been physically torn down + + Note, this can be called multiple times, and eventually with None. + + """ + + if self.cancel_on_failure: + logger.debug('Consumer is cancel_on_failure and failure seen, cancelling') + self.cancel() + + if self.state == ST_ONLINE: + # The channel has just lost operationality! + self.on_operational(False) + self.state = ST_OFFLINE + + should_retry = False + + if isinstance(payload, BasicCancel): + # Consumer Cancel Notification - by RabbitMQ + self.methods([BasicCancelOk(), ChannelClose(0, b'Received basic.cancel', 0, 0)]) + return + + if isinstance(payload, BasicCancelOk): + # OK, our cancelling went just fine - proceed with teardown + self.method(ChannelClose(0, b'Received basic.cancel-ok', 0, 0)) + return + + if isinstance(payload, ChannelClose): + rc = payload.reply_code + if rc == RESOURCE_LOCKED: + # special handling + # This is because we might be reconnecting, and the broker doesn't know yet that we are dead. + # it won't release our exclusive channels, and that's why we'll get RESOURCE_LOCKED. + + if self.fail_on_first_time_resource_locked: + # still, a RESOURCE_LOCKED on a first declaration ever suggests something is very wrong + if self.future_to_notify: + self.future_to_notify.set_exception(ResourceLocked(payload)) + self.future_to_notify = None + self.cancel() + + should_retry = True + elif rc in HARD_ERROR: + logger.warn('Channel closed due to hard error, %s: %s', payload.reply_code, payload.reply_text) + if self.future_to_notify: + self.future_to_notify.set_exception(AMQPError(payload)) + self.future_to_notify = None + + + # We might not want to throw the connection away. + should_retry = should_retry and (not self.cancelled) + + + old_con = self.connection + + super(Consumer, self).on_close(payload) # this None's self.connection + self.fail_on_first_time_resource_locked = False + + if should_retry: + if old_con.state == ST_ONLINE: + logger.info('Retrying with %s', self.queue.name) + self.attach(old_con) + + def on_delivery(self, sth): + """ + Callback for delivery-related shit + :param sth: AMQPMethodFrame WITH basic-deliver, AMQPHeaderFrame or AMQPBodyFrame + """ + if isinstance(sth, BasicDeliver): + self.receiver.on_basic_deliver(sth) + elif isinstance(sth, AMQPBodyFrame): + self.receiver.on_body(sth.data) + elif isinstance(sth, AMQPHeaderFrame): + self.receiver.on_head(sth) + + # No point in listening for more stuff, that's all the watches even listen for + + def on_setup(self, payload): + """Called with different kinds of frames - during setup""" + + if isinstance(payload, ChannelOpenOk): + # Do we need to declare the exchange? + + if self.queue.exchange is not None: + self.connection.method_and_watch( + self.channel_id, + ExchangeDeclare(self.queue.exchange.name.encode('utf8'), + self.queue.exchange.type.encode('utf8'), + False, + self.queue.exchange.durable, + self.queue.exchange.auto_delete, + False, + False, + []), + ExchangeDeclareOk, + self.on_setup + ) + else: + self.on_setup(ExchangeDeclareOk()) + + elif isinstance(payload, ExchangeDeclareOk): + # Declare the queue + + name = b'' if self.queue.anonymous else self.queue.name.encode('utf8') + + self.connection.method_and_watch( + self.channel_id, + QueueDeclare( + name, + False, + self.queue.durable, + self.queue.exclusive, + self.queue.auto_delete, + False, + [] + ), + QueueDeclareOk, + self.on_setup + ) + + elif isinstance(payload, QueueDeclareOk): + # did we need an anonymous name? + if self.queue.anonymous: + self.queue.name = payload.queue_name.decode('utf8') + + # We need any form of binding. + if self.queue.exchange is not None: + self.method_and_watch( + QueueBind( + self.queue.name.encode('utf8'), self.queue.exchange.name.encode('utf8'), + b'', False, []), + QueueBindOk, + self.on_setup + ) + else: + # default exchange, pretend it was bind ok + self.on_setup(QueueBindOk()) + elif isinstance(payload, QueueBindOk): + # itadakimasu + if self.qos is not None: + self.method(BasicQos(self.qos[0], self.qos[1], False)) + self.method_and_watch( + BasicConsume(self.queue.name.encode('utf8'), self.queue.name.encode('utf8'), + False, self.no_ack, self.queue.exclusive, False, []), + BasicConsumeOk, + self.on_setup + ) + + elif isinstance(payload, BasicConsumeOk): + # AWWW RIGHT~!!! We're good. + + # Register watches for receiving shit + self.connection.watch(HeaderOrBodyWatch(self.channel_id, self.on_delivery)) + mw = MethodWatch(self.channel_id, BasicDeliver, self.on_delivery) + mw.oneshot = False + self.connection.watch(mw) + + self.state = ST_ONLINE + self.on_operational(True) + + # resend QoS, in case of sth + if self.qos is not None: + self.set_qos(self.qos[0], self.qos[1]) + + +class MessageReceiver(object): + """This is an object that is used to received messages. + + It maintains all the state, and is used to ack/nack messages as well. + + This object is TORN DOWN when a consumer goes offline, + and is recreated when it goes online. + + This is called by consumer upon receiving different parts of the message, + and may opt to kill the connection on bad framing with + self.consumer.connection.send(None) + """ + def __init__(self, consumer): + self.consumer = consumer + self.state = 0 # 0 - waiting for Basic-Deliver + # 1 - waiting for Header + # 2 - waiting for Body [all] + # 3 - gone! + + self.bdeliver = None # payload of Basic-Deliver + self.header = None # AMQPHeaderFrame + self.body = [] # list of payloads + self.data_to_go = None # set on receiving header, how much bytes we need yet + + self.acks_pending = set() # list of things to ack/reject + + def on_gone(self): + """Called by Consumer to inform upon discarding this receiver""" + self.state = 3 + + def confirm(self, delivery_tag, success): + """ + This crafts a constructor for confirming messages. + + This should return a callable/0, whose calling will ACK or REJECT the message. + Calling it multiple times should have no ill effect. + + If this receiver is long gone, + + :param delivery_tag: delivery_tag to ack + :param success: True if ACK, False if REJECT + :return: callable/0 + """ + + def callable(): + if self.state == 3: + return # Gone! + + if self.consumer.cancelled: + return # cancelled! + + if delivery_tag not in self.acks_pending: + return # already confirmed/rejected + + if success: + self.consumer.method(BasicAck(delivery_tag, False)) + else: + self.consumer.method(BasicReject(delivery_tag, True)) + + return callable + + + def on_head(self, frame): + assert self.state == 1 + self.header = frame + self.data_to_go = frame.body_size + self.state = 2 + + def on_basic_deliver(self, payload): + assert self.state == 0 + self.bdeliver = payload + self.state = 1 + + def on_body(self, payload): + """:type payload: buffer""" + assert self.state == 2 + self.body.append(payload) + self.data_to_go -= len(payload) + assert self.data_to_go >= 0 + if self.data_to_go == 0: + ack_expected = not self.consumer.no_ack + + # Message A-OK! + + if ack_expected: + self.acks_pending.add(self.bdeliver.delivery_tag) + + from coolamqp.objects import ReceivedMessage + rm = ReceivedMessage( + b''.join(map(six.binary_type, self.body)), #todo inefficient as FUUUCK + self.bdeliver.exchange, + self.bdeliver.routing_key, + self.header.properties, + self.bdeliver.delivery_tag, + None if self.consumer.no_ack else self.confirm(self.bdeliver.delivery_tag, True), + None if self.consumer.no_ack else self.confirm(self.bdeliver.delivery_tag, False), + ) + + self.consumer.on_message(rm) + + self.state = 0 + + # at this point it's safe to clear the body + self.body = [] diff --git a/coolamqp/attaches/publisher.py b/coolamqp/attaches/publisher.py new file mode 100644 index 0000000000000000000000000000000000000000..147df0fc5705a95d3700f42fafbc1da8879bb7d1 --- /dev/null +++ b/coolamqp/attaches/publisher.py @@ -0,0 +1,245 @@ +# coding=utf-8 +""" +Module used to publish messages. + +Expect wild NameErrors if you build this without RabbitMQ extensions (enabled by default), +and try to use MODE_CNPUB. + +If you use a broker that doesn't support these, just don't use MODE_CNPUB. CoolAMQP is smart enough +to check with the broker beforehand. +""" +from __future__ import absolute_import, division, print_function + +import collections +import logging +import struct +import warnings + +from coolamqp.framing.definitions import ChannelOpenOk, BasicPublish, Basic, BasicAck +from coolamqp.framing.frames import AMQPMethodFrame, AMQPBodyFrame, AMQPHeaderFrame + +try: + # these extensions will be available + from coolamqp.framing.definitions import ConfirmSelect, ConfirmSelectOk, BasicNack +except ImportError: + pass + +from coolamqp.attaches.channeler import Channeler, ST_ONLINE, ST_OFFLINE +from coolamqp.uplink import PUBLISHER_CONFIRMS, MethodWatch, FailWatch +from coolamqp.attaches.utils import AtomicTagger, FutureConfirmableRejectable, Synchronized + +from coolamqp.objects import Future + +logger = logging.getLogger(__name__) + + +# for holding messages when MODE_CNPUB and link is down +CnpubMessageSendOrder = collections.namedtuple('CnpubMessageSendOrder', ('message', 'exchange_name', + 'routing_key', 'future')) + + +class Publisher(Channeler, Synchronized): + """ + An object that is capable of sucking into a Connection and sending messages. + Depending on it's characteristic, it may process messages in: + + - non-ack mode (default) - messages will be dropped on the floor if there is no active uplink + - Consumer Publish mode - requires broker support, each message will be ACK/NACKed by the broker + messages will survive broker reconnections. + + If you support this, it is your job to ensure that broker supports + publisher_confirms. If it doesn't, this publisher will enter ST_OFFLINE + and emit a warning. + + Other modes may be added in the future. + + Since this may be called by other threads than ListenerThread, this has locking. + + _pub and on_fail are synchronized so that _pub doesn't see a partially destroyed class. + """ + MODE_NOACK = 0 # no-ack publishing + MODE_CNPUB = 1 # RabbitMQ publisher confirms extension + #todo add fallback using plain AMQP transactions - this will remove UnusablePublisher and stuff + + + class UnusablePublisher(Exception): + """This publisher will never work (eg. MODE_CNPUB on a broker not supporting publisher confirms)""" + + def __init__(self, mode): + """ + Create a new publisher + :param mode: Publishing mode to use. One of: + MODE_NOACK - use non-ack mode + MODE_CNPUB - use consumer publishing mode. A switch to MODE_TXPUB will be made + if broker does not support these. + :raise ValueError: mode invalid + """ + Channeler.__init__(self) + Synchronized.__init__(self) + + if mode not in (Publisher.MODE_NOACK, Publisher.MODE_CNPUB): + raise ValueError(u'Invalid publisher mode') + + self.mode = mode + + self.messages = collections.deque() # Messages to publish. From newest to last. + # tuple of (Message object, exchange name::str, routing_key::str, + # Future to confirm or None, flags as tuple|empty tuple + + self.tagger = None # None, or AtomicTagger instance id MODE_CNPUB + + self.critically_failed = False + + @Synchronized.synchronized + def attach(self, connection): + Channeler.attach(self, connection) + connection.watch(FailWatch(self.on_fail)) + + @Synchronized.synchronized + def on_fail(self): + self.state = ST_OFFLINE + print('Publisher is FAILED') + + def _pub(self, message, exchange_name, routing_key): + """ + Just send the message. Sends BasicDeliver + header + body. + + BECAUSE OF publish THIS CAN GET CALLED BY FOREIGN THREAD. + + :param message: Message instance + :param exchange_name: exchange to use + :param routing_key: routing key to use + :type exchange_name: bytes + :param routing_key: bytes + """ + # Break down large bodies + bodies = [] + + body = buffer(message.body) + max_body_size = self.connection.frame_max - AMQPBodyFrame.FRAME_SIZE_WITHOUT_PAYLOAD + while len(body) > 0: + bodies.append(buffer(body, 0, max_body_size)) + body = buffer(body, max_body_size) + + self.connection.send([ + AMQPMethodFrame(self.channel_id, BasicPublish(exchange_name, routing_key, False, False)), + AMQPHeaderFrame(self.channel_id, Basic.INDEX, 0, len(message.body), message.properties) + ]) + + # todo optimize it - if there's only one frame it can with previous send + for body in bodies: + self.connection.send([AMQPBodyFrame(self.channel_id, body)]) + + def _mode_cnpub_process_deliveries(self): + """ + Dispatch all frames that are waiting to be sent + + To be used when mode is MODE_CNPUB and we just got ST_ONLINE + """ + assert self.state == ST_ONLINE + assert self.mode == Publisher.MODE_CNPUB + + while len(self.messages) > 0: + msg, xchg, rk, fut = self.messages.popleft() + + if fut.cancelled: + # Ok, don't do this. + fut.set_cancel() + continue + + self.tagger.deposit(self.tagger.get_key(), FutureConfirmableRejectable(fut)) + self._pub(msg, xchg, rk) + + def _on_cnpub_delivery(self, payload): + """ + This gets called on BasicAck and BasicNack, if mode is MODE_CNPUB + """ + assert self.mode == Publisher.MODE_CNPUB + + print('Got %s with dt=%s' % (payload, payload.delivery_tag)) + + if isinstance(payload, BasicAck): + self.tagger.ack(payload.delivery_tag, payload.multiple) + elif isinstance(payload, BasicNack): + self.tagger.nack(payload.delivery_tag, payload.multiple) + + @Synchronized.synchronized + def publish(self, message, exchange_name=b'', routing_key=b''): + """ + Schedule to have a message published. + + If mode is MODE_CNPUB: + this function will return a Future. Future can end either with success (result will be None), + or exception (a plain Exception instance). Exception will happen when broker NACKs the message: + that, according to RabbitMQ, means an internal error in Erlang process. + + Returned Future can be cancelled - this will prevent from sending the message, if it hasn't commenced yet. + + If mode is MODE_NOACK: + this function returns None. Messages are dropped on the floor if there's no connection. + + :param message: Message object to send + :param exchange_name: exchange name to use. Default direct exchange by default + :param routing_key: routing key to use + :return: a Future instance, or None + :raise Publisher.UnusablePublisher: this publisher will never work (eg. MODE_CNPUB on Non-RabbitMQ) + """ + # Formulate the request + if self.mode == Publisher.MODE_NOACK: + # If we are not connected right now, drop the message on the floor and log it with DEBUG + if self.state != ST_ONLINE: + logger.debug(u'Publish request, but not connected - dropping the message') + else: + self._pub(message, exchange_name, routing_key) + + elif self.mode == Publisher.MODE_CNPUB: + fut = Future() + + #todo can optimize this not to create an object if ST_ONLINE already + cnpo = CnpubMessageSendOrder(message, exchange_name, routing_key, fut) + self.messages.append(cnpo) + + if self.state == ST_ONLINE: + self._mode_cnpub_process_deliveries() + + return fut + else: + raise Exception(u'Invalid mode') + + def on_setup(self, payload): + + # Assert that mode is OK + if self.mode == Publisher.MODE_CNPUB: + if PUBLISHER_CONFIRMS not in self.connection.extensions: + warnings.warn(u'Broker does not support publisher_confirms, refusing to start publisher', + RuntimeWarning) + self.state = ST_OFFLINE + self.critically_failed = True + return + + if isinstance(payload, ChannelOpenOk): + # Ok, if this has a mode different from MODE_NOACK, we need to additionally set up + # the functionality. + + if self.mode == Publisher.MODE_CNPUB: + self.method_and_watch(ConfirmSelect(False), ConfirmSelectOk, self.on_setup) + elif self.mode == Publisher.MODE_NOACK: + # A-OK! Boot it. + self.state = ST_ONLINE + self.on_operational(True) + + elif self.mode == Publisher.MODE_CNPUB: + # Because only in this case it makes sense to check for MODE_CNPUB + if isinstance(payload, ConfirmSelectOk): + # A-OK! Boot it. + self.state = ST_ONLINE + self.on_operational(True) + + self.tagger = AtomicTagger() + + # now we need to listen for BasicAck and BasicNack + + mw = MethodWatch(self.channel_id, (BasicAck, BasicNack), self._on_cnpub_delivery) + mw.oneshot = False + self.connection.watch(mw) + self._mode_cnpub_process_deliveries() diff --git a/coolamqp/attaches/utils.py b/coolamqp/attaches/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..9989ca77f47edc846386c44312849dbd9348372c --- /dev/null +++ b/coolamqp/attaches/utils.py @@ -0,0 +1,233 @@ +# coding=UTF-8 +from __future__ import print_function, absolute_import, division +import six +import logging +import threading +import functools + +logger = logging.getLogger(__name__) + + +class ConfirmableRejectable(object): + """ + Protocol for objects put into AtomicTagger. You need not subclass it, + just support this protocol. + """ + + def confirm(self): + """ + This has been ACK'd + :return: don't care + """ + + def reject(self): + """ + This has been REJECT'd/NACK'd + :return: don't care + """ + +class FutureConfirmableRejectable(ConfirmableRejectable): + """ + A ConfirmableRejectable that can result a future (with None), + or Exception it with a message + """ + def __init__(self, future): + self.future = future + + def confirm(self): + self.future.set_result() + + def reject(self): + self.future.set_exception(Exception()) + + +class AtomicTagger(object): + """ + This implements a thread-safe dictionary of (integer=>ConfirmableRejectable | None), + used for processing delivery tags / (negative) acknowledgements. + - you can requisition a key. This key belongs only to you, and the whole world + doesn't know you have it. + + delivery_tag_to_use = tagger.get_key() + + - you can deposit a ConfirmableRejectable into the tagger. + + tagger.deposit(delivery_tag, message) + + After you do so, this tag is subject to be acked/nacked. Read on. + + - you can (multiple)(ack/nack) messages. This coresponds to multiple bit + used in basic.ack/basic.nack. + + If this is done, your message objects (that MUST implement the + ConfirmableRejectable protocol) will have respective methods called. + These methods MUST NOT depend on particular state of locking by this + object. + + Thread safety is implemented using reentrant locking. The lock object is a + threading.RLock, and you can access it at atomicTagger.lock. + + Please note that delivery tags are increasing non-negative integer. + Therefore, X>Y implies that sending/receiving X happened after Y. + + Note that key/delivery_tag of 0 has special meaning of "everything so far". + + This has to be fast for most common cases. Corner cases will be resolved correctly, + but maybe not fast. + """ + + def __init__(self): + self.lock = threading.RLock() + + # Protected by lock + self.next_tag = 1 # 0 is AMQP-reserved to mean "everything so far" + self.tags = [] # a list of (tag, ConfirmableRejectable) + # they remain to be acked/nacked + # invariant: FOR EACH i, j: (i>j) => (tags[i][0] > tags[j][0]) + + def deposit(self, tag, obj): + """ + Put a tag into the tag list. + + Putting the same tag more than one time will result in undefined behaviour. + + :param tag: non-negative integer + :param obj: ConfirmableRejectable + if you put something that isn't a ConfirmableRejectable, you won't get bitten + until you call .ack() or .nack(). + """ + assert tag >= 0 + opt = (tag, obj) + + with self.lock: + if len(self.tags) == 0: + self.tags.append(opt) + elif self.tags[-1][0] < tag: + self.tags.append(opt) + else: + # Insert a value at place where it makes sense. Iterate from the end, because + # values will usually land there... + i = len(self.tags) - 1 # start index + + while i>0: # this will terminate at i=0 + if self.tags[i][0] > tag: # this means we should insert it here... + break + i -= 1 # previousl index + + self.tags.insert(i, opt) + + def __acknack(self, tag, multiple, ack): + """ + :param tag: Note that 0 means "everything" + :param ack: True to ack, False to nack + """ + # Compute limits - they go from 0 to somewhere + with self.lock: + start = 0 + # start and stop will signify the PYTHON SLICE parameters + + if tag > 0: + + if multiple: + # Compute the ranges + for stop, opt in enumerate(self.tags): + if opt[0] == tag: + stop += 1 # this is exactly this tag. Adjust stop to end one further (Python slicing) and stop + break + if opt[0] > tag: + break # We went too far, but it's OK, we don't need to bother with adjusting stop + else: + # List finished without breaking? That would mean the entire range! + stop = len(self.tags) + else: + # Just find that piece + for index, opt in enumerate(self.tags): + if opt[0] == tag: + stop = index + 1 + break + else: + return # not found! + + + if not multiple: + start = stop-1 + else: + # Oh, I know the range! + stop = len(self.tags) + + print('Range computed of %s:%s' % (start, stop)) + + items = self.tags[start:stop] + del self.tags[start:stop] + + for tag, cr in items: + if ack: + cr.confirm() + else: + cr.reject() + + def ack(self, tag, multiple): + """ + Acknowledge given objects. + + If multiple, objects UP TO AND INCLUDING tag will have .confirm() called. + If it's false, only this precise objects will have done so. + It this object does not exist, nothing will happen. Acking same tag more than one time + is a no-op. + + Things acked/nacked will be evicted from .data + :param tag: delivery tag to use. Note that 0 means "everything so far" + """ + self.__acknack(tag, multiple, True) + + def nack(self, tag, multiple): + """ + Acknowledge given objects. + + If multiple, objects UP TO AND INCLUDING tag will have .confirm() called. + If it's false, only this precise objects will have done so. + It this object does not exist, nothing will happen. Acking same tag more than one time + is a no-op. + + Things acked/nacked will be evicted from .data + :param tag: delivery tag to use. Note that 0 means "everything so far" + """ + self.__acknack(tag, multiple, False) + + def get_key(self): + """ + Return a key. It won't be seen here until you deposit it. + + It's just yours, and you can do whatever you want with it, even drop on the floor. + :return: a positive integer + """ + with self.lock: + self.next_tag += 1 + return self.next_tag - 1 + + +class Synchronized(object): + """ + I have a lock and can sync on it. Use like: + + class Synced(Synchronized): + + @synchronized + def mandatorily_a_instance_method(self, ...): + ... + + """ + + def __init__(self): + self._monitor_lock = threading.Lock() + + @staticmethod + def synchronized(fun): + @functools.wraps(fun) + def monitored(*args, **kwargs): + with args[0]._monitor_lock: + return fun(*args, **kwargs) + + return monitored + + diff --git a/coolamqp/backends/__init__.py b/coolamqp/backends/__init__.py deleted file mode 100644 index 77106aaea6de30fafc10248afa1fc5d5ec8e89d0..0000000000000000000000000000000000000000 --- a/coolamqp/backends/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# coding=UTF-8 -from coolamqp.backends.pyamqp import PyAMQPBackend -from coolamqp.backends.base import AMQPError, ConnectionFailedError, RemoteAMQPError, Cancelled diff --git a/coolamqp/backends/base.py b/coolamqp/backends/base.py deleted file mode 100644 index ad1b34ff77fff1029b704697ae0012218c7c2b2e..0000000000000000000000000000000000000000 --- a/coolamqp/backends/base.py +++ /dev/null @@ -1,145 +0,0 @@ -# coding=UTF-8 -class AMQPError(Exception): - """Connection errors and bawking of AMQP server""" - code = None - reply_text = 'AMQP error' - - def __repr__(self): - return u'AMQPError()' - - -class ConnectionFailedError(AMQPError): - """Connection to broker failed""" - reply_text = 'failed connecting to broker' - - def __repr__(self): - return u'ConnectionFailedError("%s")' % map(repr, (self.reply_text, )) - - -class Discarded(Exception): - """send() for this message had discard_on_retry""" - - -class Cancelled(Exception): - """Cancel ordered by user""" - - -class RemoteAMQPError(AMQPError): - """ - Remote AMQP broker responded with an error code - """ - def __init__(self, code, text=None): - """ - :param code: AMQP error code - :param text: AMQP error text (optional) - """ - AMQPError.__init__(self, text) - self.code = code - self.text = text or 'server sent back an error' - - def __repr__(self): - return u'RemoteAMQPError(%s, %s)' % map(repr, (self.code, self.text)) - -class AMQPBackend(object): - """ - Dummy AMQP backend. - - Every method may raise either ConnectionFailedError (if connection failed) - or RemoteAMQPError (if broker returned an error response) - """ - - def __init__(self, cluster_node, cluster_handler_thread): - """ - Connects to an AMQP backend. - """ - self.cluster_handler_thread = cluster_handler_thread - - def process(self, max_time=10): - """ - Do bookkeeping, process messages, etc. - :param max_time: maximum time in seconds this call can take - :raises ConnectionFailedError: if connection failed in the meantime - """ - - def exchange_declare(self, exchange): - """ - Declare an exchange - :param exchange: Exchange object - """ - - def exchange_delete(self, exchange): - """ - Delete an exchange - :param exchange: Exchange object - """ - - def queue_bind(self, queue, exchange, routing_key=''): - """ - Bind a queue to an exchange - :param queue: Queue object - :param exchange: Exchange object - :param routing_key: routing key to use - """ - - def queue_delete(self, queue): - """ - Delete a queue. - - :param queue: Queue - """ - - - def queue_declare(self, queue): - """ - Declare a queue. - - This will change queue's name if anonymous - :param queue: Queue - """ - - def basic_cancel(self, consumer_tag): - """ - Cancel consuming, identified by a consumer_tag - :param consumer_tag: consumer_tag to cancel - """ - - def basic_consume(self, queue, no_ack=False): - """ - Start consuming from a queue - :param queue: Queue object - :param no_ack: Messages will not need to be ack()ed for this queue - """ - - def basic_ack(self, delivery_tag): - """ - ACK a message. - :param delivery_tag: delivery tag to ack - """ - - def basic_qos(self, prefetch_size, prefetch_count, global_): - """ - Issue a basic.qos(prefetch_size, prefetch_count, True) against broker - :param prefetch_size: prefetch window size in octets - :param prefetch_count: prefetch window in terms of whole messages - """ - - def basic_reject(self, delivery_tag): - """ - Reject a message - :param delivery_tag: delivery tag to reject - """ - - def basic_publish(self, message, exchange, routing_key): - """ - Send a message - :param message: Message object to send - :param exchange: Exchange object to publish to - :param routing_key: routing key to use - """ - - def shutdown(self): - """ - Close this connection. - This is not allowed to return anything or raise - """ - self.cluster_handler_thread = None # break GC cycles diff --git a/coolamqp/backends/pyamqp.py b/coolamqp/backends/pyamqp.py deleted file mode 100644 index b452bb8345f2d349c09feefeb13fd7c4e60c0889..0000000000000000000000000000000000000000 --- a/coolamqp/backends/pyamqp.py +++ /dev/null @@ -1,173 +0,0 @@ -# coding=UTF-8 -"""Backend using pyamqp""" -from __future__ import division -import amqp -import socket -import six -import functools -import logging -from coolamqp.backends.base import AMQPBackend, RemoteAMQPError, ConnectionFailedError -import monotonic - - -logger = logging.getLogger(__name__) - - -def translate_exceptions(fun): - """ - Translates pyamqp's exceptions to CoolAMQP's - - py-amqp's exceptions are less than intuitive, so expect many special cases - """ - @functools.wraps(fun) - def q(*args, **kwargs): - try: - return fun(*args, **kwargs) - except (amqp.exceptions.ConsumerCancelled): - # I did not expect those here. Channel must be really bad. - logger.critical('Found consumer cancelled where it should not be') - raise ConnectionFailedError('WTF: '+(e.message if six.PY2 else e.args[0])) - except (amqp.RecoverableChannelError, - amqp.exceptions.NotFound, - amqp.exceptions.NoConsumers, - amqp.exceptions.ResourceLocked, - amqp.exceptions.ResourceError, - amqp.exceptions.ResourceLocked, - amqp.exceptions.AccessRefused) as e: - logger.warn('py-amqp: backend reports %s', repr(e)) - raise RemoteAMQPError(e.reply_code, e.reply_text) - except (IOError, - amqp.ConnectionForced, - amqp.exceptions.InvalidPath, - amqp.IrrecoverableChannelError, - amqp.exceptions.UnexpectedFrame) as e: - logger.warn('py-amqp: backend reports %s', repr(e)) - raise ConnectionFailedError(e.message if six.PY2 else e.args[0]) - return q - - -class PyAMQPBackend(AMQPBackend): - @translate_exceptions - def __init__(self, node, cluster_handler_thread): - AMQPBackend.__init__(self, node, cluster_handler_thread) - - self.connection = amqp.Connection(host=node.host, - userid=node.user, - password=node.password, - virtual_host=node.virtual_host, - heartbeat=node.heartbeat or 0) - try: - self.connection.connect() #todo what does this raise? - except AttributeError: - pass # this does not always have to exist - self.channel = self.connection.channel() - self.channel.auto_decode = False - self.heartbeat = node.heartbeat or 0 - self.last_heartbeat_at = monotonic.monotonic() - - def shutdown(self): - AMQPBackend.shutdown(self) - print 'BACKEND SHUTDOWN START' - try: - self.channel.close() - except: - pass - try: - self.connection.close() - except: - pass - print 'BACKEND SHUTDOWN COMPLETE' - - @translate_exceptions - def process(self, max_time=1): - try: - if self.heartbeat > 0: - if monotonic.monotonic() - self.last_heartbeat_at > (self.heartbeat / 2): - self.connection.heartbeat_tick(rate=self.heartbeat) - self.last_heartbeat_at = monotonic.monotonic() - self.connection.drain_events(max_time) - except socket.timeout as e: - pass - - @translate_exceptions - def basic_cancel(self, consumer_tag): - self.channel.basic_cancel(consumer_tag) - - @translate_exceptions - def basic_publish(self, message, exchange, routing_key): - # convert this to pyamqp's Message - a = amqp.Message(six.binary_type(message.body), - **message.properties) - - self.channel.basic_publish(a, exchange=exchange.name, routing_key=routing_key) - - @translate_exceptions - def exchange_declare(self, exchange): - self.channel.exchange_declare(exchange.name, exchange.type, durable=exchange.durable, - auto_delete=exchange.auto_delete) - - @translate_exceptions - def queue_bind(self, queue, exchange, routing_key=''): - self.channel.queue_bind(queue.name, exchange.name, routing_key) - - @translate_exceptions - def basic_ack(self, delivery_tag): - self.channel.basic_ack(delivery_tag, multiple=False) - - @translate_exceptions - def exchange_delete(self, exchange): - self.channel.exchange_delete(exchange.name) - - @translate_exceptions - def basic_qos(self, prefetch_size, prefetch_count, global_): - self.channel.basic_qos(prefetch_size, prefetch_count, global_) - - @translate_exceptions - def queue_delete(self, queue): - self.channel.queue_delete(queue.name) - - @translate_exceptions - def basic_reject(self, delivery_tag): - self.channel.basic_reject(delivery_tag, True) - - @translate_exceptions - def queue_declare(self, queue): - """ - Declare a queue. - - This will change queue's name if anonymous - :param queue: Queue - """ - if queue.anonymous: - queue.name = '' - - qname, mc, cc = self.channel.queue_declare(queue.name, - durable=queue.durable, - exclusive=queue.exclusive, - auto_delete=queue.auto_delete) - if queue.anonymous: - queue.name = qname - - @translate_exceptions - def basic_consume(self, queue, no_ack=False): - """ - Start consuming from a queue - :param queue: Queue object - """ - self.channel.basic_consume(queue.name, - consumer_tag=queue.consumer_tag, - exclusive=queue.exclusive, - no_ack=no_ack, - callback=self.__on_message, - on_cancel=self.__on_consumercancelled) - - def __on_consumercancelled(self, consumer_tag): - self.cluster_handler_thread._on_consumercancelled(consumer_tag) - - def __on_message(self, message): - assert isinstance(message.body, six.binary_type) - self.cluster_handler_thread._on_recvmessage(message.body, - message.delivery_info['exchange'], - message.delivery_info['routing_key'], - message.delivery_info['delivery_tag'], - message.properties) diff --git a/coolamqp/cluster.py b/coolamqp/cluster.py deleted file mode 100644 index 21bbb37e85f831ee7bfebb916245c30bec883ab8..0000000000000000000000000000000000000000 --- a/coolamqp/cluster.py +++ /dev/null @@ -1,240 +0,0 @@ -# coding=UTF-8 -import itertools -from six.moves import queue as Queue -from coolamqp.backends import PyAMQPBackend -from coolamqp.backends.base import Discarded -from coolamqp.orders import SendMessage, ConsumeQueue, DeclareExchange, CancelQueue, DeleteQueue, \ - DeleteExchange, SetQoS, DeclareQueue, Order -from coolamqp.messages import Exchange - - -class ClusterNode(object): - """ - Definition of a reachable AMQP node. - - This object is hashable. - """ - - def __init__(self, *args, **kwargs): - """ - Create a cluster node definition. - - a = ClusterNode(host='192.168.0.1', user='admin', password='password', - virtual_host='vhost') - - or - - a = ClusterNode('192.168.0.1', 'admin', 'password') - - Additional keyword parameters that can be specified: - heartbeat - heartbeat interval in seconds - """ - - self.heartbeat = kwargs.pop('heartbeat', None) - - if len(kwargs) > 0: - # Prepare arguments for amqp.connection.Connection - self.host = kwargs['host'] - self.user = kwargs['user'] - self.password = kwargs['password'] - self.virtual_host = kwargs.get('virtual_host', '/') - elif len(args) == 3: - self.host, self.user, self.password = args - self.virtual_host = '/' - elif len(args) == 4: - self.host, self.user, self.password, self.virtual_host = args - else: - raise NotImplementedError #todo implement this - - def __str__(self): - return '%s@%s/%s' % (self.host, - self.user, - self.virtual_host) - - -class Cluster(object): - """ - Represents connection to an AMQP cluster. This internally connects only to one node, but - will select another one upon connection failing. - - You can pass callbacks to most commands. They will also return an Order instance, - that you can wait for to know an operation has completed. - - Callbacks are executed before Order is marked as complete (it's .result() returns), so if you do: - - cluster.send(.., on_completed=hello).result() - bye() - - hello will be called before bye is called. - """ - - def __init__(self, nodes, backend=PyAMQPBackend): - """ - Construct the cluster definition - :param nodes: iterable of nodes to try connecting, in this order. - if list if exhaused, it will be started from beginning - :param backend: backend to use - """ - - self.backend = backend - self.node_to_connect_to = itertools.cycle(nodes) - - self.connected = False #: public, is connected to broker? - - from .handler import ClusterHandlerThread - self.thread = ClusterHandlerThread(self) - - def send(self, message, exchange=None, routing_key='', discard_on_fail=False, on_completed=None, on_failed=None): - """ - Schedule a message to be sent. - :param message: Message object to send. - :param exchange: Exchange to use. Leave None to use the default exchange - :param routing_key: routing key to use - :param discard_on_fail: if True, then message is valid for sending ONLY with current connection. - Will be discarded upon fail. - :param on_completed: callable/0 to call when this succeeds - :param on_failed: callable/1 to call when this fails with AMQPError instance - or Cancelled instance if user cancelled this order - or Discarded instance if message discarded due to 'discard_on_fail' - :return: a Future with this order's status - """ - a = SendMessage(message, exchange or Exchange.direct, routing_key, - discard_on_fail=discard_on_fail, - on_completed=on_completed, on_failed=on_failed) - - if discard_on_fail and self.thread.backend is None: - o = Order() - o.discarded = True - on_failed(Discarded()) - return o - # discard at once if no point in sending - - self.thread.order_queue.append(a) - return a - - def declare_exchange(self, exchange, on_completed=None, on_failed=None): - """ - Declare an exchange. It will be re-declared upon reconnection. - - :param exchange: Exchange to declare - :param on_completed: callable/0 to call when this succeeds - :param on_failed: callable/1 to call when this fails with AMQPError instance - :return: a Future with this order's status - """ - a = DeclareExchange(exchange, on_completed=on_completed, on_failed=on_failed) - self.thread.order_queue.append(a) - return a - - def declare_queue(self, queue, on_completed=None, on_failed=None): - """ - Declares a queue. - - !!!! If you declare a queue and NOT consume from it, it will not be re-declared - upon reconnection !!!! - - :param queue: Queue to declare - :param on_completed: callable/0 to call when this succeeds - :param on_failed: callable/1 to call when this fails with AMQPError instance - :return: a Future with this order's status - """ - a = DeclareQueue(queue, on_completed=on_completed, on_failed=on_failed) - self.thread.order_queue.append(a) - return a - - def delete_exchange(self, exchange, on_completed=None, on_failed=None): - """ - Delete an exchange - :param exchange: Exchange to delete - :param on_completed: callable/0 to call when this succeeds - :param on_failed: callable/1 to call when this fails with AMQPError instance - :return: a Future with this order's status - """ - a = DeleteExchange(exchange, on_completed=on_completed, on_failed=on_failed) - self.thread.order_queue.append(a) - return a - - def delete_queue(self, queue, on_completed=None, on_failed=None): - """ - Delete a queue - :param queue: Queue to delete - :param on_completed: callable/0 to call when this succeeds - :param on_failed: callable/1 to call when this fails with AMQPError instance - :return: a Future with this order's status - """ - a = DeleteQueue(queue, on_completed=on_completed, on_failed=on_failed) - self.thread.order_queue.append(a) - return a - - def cancel(self, queue, on_completed=None, on_failed=None): - """ - Cancel consuming from a queue - - :param queue: Queue to consume from - :param on_completed: callable/0 to call when this succeeds - :param on_failed: callable/1 to call when this fails with AMQPError instance - :return: a Future with this order's status - """ - a = CancelQueue(queue, on_completed=on_completed, on_failed=on_failed) - self.thread.order_queue.append(a) - return a - - def qos(self, prefetch_window, prefetch_count, global_=True): - a = SetQoS(prefetch_window, prefetch_count, global_) - self.thread.order_queue.append(a) - return a - - def consume(self, queue, no_ack=False, on_completed=None, on_failed=None): - """ - Start consuming from a queue - - This queue will be declared to the broker. If this queue has any binds - (.exchange field is not empty), queue will be binded to exchanges. - - :param queue: Queue to consume from - :param on_completed: callable/0 to call when this succeeds - :param no_ack: if True, you will not need to call .ack() for this queue - :param on_failed: callable/1 to call when this fails with AMQPError instance - :return: a Future with this order's status - """ - a = ConsumeQueue(queue, no_ack=no_ack, on_completed=on_completed, on_failed=on_failed) - self.thread.order_queue.append(a) - return a - - def drain(self, wait=0): - """ - Return a ClusterEvent on what happened, or None if nothing could be obtained - within given time - :param wait: Interval to wait for events. - Finite number to wait this much seconds before returning None - None to wait for infinity - 0 to return immediately - :return: a ClusterEvent instance or None - """ - try: - if wait == 0: - return self.thread.event_queue.get(False) - else: - return self.thread.event_queue.get(True, wait) - except Queue.Empty: - return None - - def start(self): - """ - Connect to the cluster. - :return: self - """ - self.thread.start() - return self - - def shutdown(self, complete_remaining_tasks=False): - """ - Cleans everything and returns. - - :param complete_remaining_tasks_tasks: if set to True, pending operations will be completed. - If False, thread will exit without completing them. - This can mean that if the cluster doesn't come up online, shutdown MAY BLOCK FOREVER. - """ - self.thread.complete_remaining_upon_termination = complete_remaining_tasks - self.thread.terminate() - self.thread.join() - # thread closes the AMQP uplink for us diff --git a/coolamqp/clustering/__init__.py b/coolamqp/clustering/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..337b30fa20106d995704f7210238884b0fe5c5a8 --- /dev/null +++ b/coolamqp/clustering/__init__.py @@ -0,0 +1,13 @@ +# coding=UTF-8 +""" +This is the layer that you talk to. It abstracts away one (in future - more) connections +to broker with an uniform interface. +""" +from __future__ import print_function, absolute_import, division + +import logging + +logger = logging.getLogger(__name__) + + +from coolamqp.clustering.cluster import Cluster diff --git a/coolamqp/clustering/cluster.py b/coolamqp/clustering/cluster.py new file mode 100644 index 0000000000000000000000000000000000000000..9d170b17679ab9a694c4078b3b73d60422a55e93 --- /dev/null +++ b/coolamqp/clustering/cluster.py @@ -0,0 +1,131 @@ +# coding=UTF-8 +""" +THE object you interface with +""" +from __future__ import print_function, absolute_import, division +import six +import logging +import warnings +import time +from coolamqp.uplink import ListenerThread +from coolamqp.clustering.single import SingleNodeReconnector +from coolamqp.attaches import Publisher, AttacheGroup, Consumer +from coolamqp.objects import Future, Exchange +from six.moves.queue import Queue + + +logger = logging.getLogger(__name__) + + +class Cluster(object): + """ + Frontend for your AMQP needs. + + This has ListenerThread. + + Call .start() to connect to AMQP. + """ + + # Events you can be informed about + ST_LINK_LOST = 0 # Link has been lost + ST_LINK_REGAINED = 1 # Link has been regained + + + def __init__(self, nodes): + """ + :param nodes: list of nodes, or a single node. For now, only one is supported. + :type nodes: NodeDefinition instance or a list of NodeDefinition instances + """ + from coolamqp.objects import NodeDefinition + if isinstance(nodes, NodeDefinition): + nodes = [nodes] + + if len(nodes) > 1: + raise NotImplementedError(u'Multiple nodes not supported yet') + + self.listener = ListenerThread() + self.node, = nodes + + self.attache_group = AttacheGroup() + + self.snr = SingleNodeReconnector(self.node, self.attache_group, self.listener) + + # Spawn a transactional publisher and a noack publisher + self.pub_tr = Publisher(Publisher.MODE_CNPUB) + self.pub_na = Publisher(Publisher.MODE_NOACK) + + self.attache_group.add(self.pub_tr) + self.attache_group.add(self.pub_na) + + self.events = Queue() # for + + def consume(self, queue, on_message=None, *args, **kwargs): + """ + Start consuming from a queue. + + args and kwargs will be passed to Consumer constructor (coolamqp.attaches.consumer.Consumer). + Don't use future_to_notify - it's done here! + + Take care not to lose the Consumer object - it's the only way to cancel a consumer! + + :param queue: Queue object, being consumed from right now. + Note that name of anonymous queue might change at any time! + :param on_message: callable that will process incoming messages + if you leave it at None, messages will be .put into self.events + :type on_message: callable(ReceivedMessage instance) or None + :return: a tuple (Consumer instance, and a Future), that tells, when consumer is ready + """ + fut = Future() + on_message = on_message or self.events.put_nowait + con = Consumer(queue, on_message, future_to_notify=fut, *args, **kwargs) + self.attache_group.add(con) + return con, fut + + def publish(self, message, exchange=None, routing_key=u'', tx=False): + """ + Publish a message. + + :param message: Message to publish + :param exchange: exchange to use. Default is the "direct" empty-name exchange. + :type exchange: unicode/bytes (exchange name) or Exchange object. + :param routing_key: routing key to use + :param tx: Whether to publish it transactionally. + If you choose so, you will receive a Future that can be used + to check it broker took responsibility for this message. + :return: Future or None + """ + + publisher = (self.pub_tr if tx else self.pub_na) + + if isinstance(exchange, Exchange): + exchange = exchange.name + + try: + return publisher.publish(message, exchange.encode('utf8'), routing_key.encode('utf8')) + except Publisher.UnusablePublisher: + raise NotImplementedError(u'Sorry, this functionality if not yet implemented!') + + + def start(self, wait=True): + """ + Connect to broker. + :param wait: block until connection is ready + """ + self.listener.start() + self.snr.connect() + + #todo not really elegant + if wait: + while not self.snr.is_connected(): + time.sleep(0.1) + + def shutdown(self, wait=True): + """ + Terminate all connections, release resources - finish the job. + :param wait: block until this is done + """ + + self.snr.shutdown() + self.listener.terminate() + if wait: + self.listener.join() diff --git a/coolamqp/clustering/single.py b/coolamqp/clustering/single.py new file mode 100644 index 0000000000000000000000000000000000000000..9fbb9e2683e8dd4c71ce7e977edee500c0cbd47a --- /dev/null +++ b/coolamqp/clustering/single.py @@ -0,0 +1,40 @@ +# coding=UTF-8 +from __future__ import print_function, absolute_import, division +import six +import logging + +from coolamqp.uplink import Connection + +logger = logging.getLogger(__name__) + + +class SingleNodeReconnector(object): + """ + Connection to one node. It will do it's best to remain alive. + """ + + def __init__(self, node_def, attache_group, listener_thread): + self.listener_thread = listener_thread + self.node_def = node_def + self.attache_group = attache_group + self.connection = None + + def is_connected(self): + return self.connection is not None + + def connect(self): + assert self.connection is None + + # Initiate connecting - this order is very important! + self.connection = Connection(self.node_def, self.listener_thread) + self.attache_group.attach(self.connection) + self.connection.start() + self.connection.add_finalizer(self.on_fail) + + def on_fail(self): + self.connection = None + self.connect() + + def shutdown(self): + """Close this connection""" + self.connection.send(None) diff --git a/coolamqp/events.py b/coolamqp/events.py deleted file mode 100644 index 0dbe85087adab5dc48779423a53bafe8842bb5e6..0000000000000000000000000000000000000000 --- a/coolamqp/events.py +++ /dev/null @@ -1,53 +0,0 @@ -# coding=UTF-8 -""" -Events emitted by Cluster -""" - - -class ClusterEvent(object): - """Base class for events emitted by cluster""" - - -class ConnectionDown(ClusterEvent): - """Connection to broker has been broken""" - - -class ConnectionUp(ClusterEvent): - """Connection to broker has been (re)established""" - - def __init__(self, initial=False): - self.initial = initial #: public, is this first connection up in this cluster ever? - - -class MessageReceived(ClusterEvent): - """A message has been received from the broker""" - def __init__(self, message): - """ - :param message: ReceivedMessage instance - """ - self.message = message - - -class ConsumerCancelled(ClusterEvent): - """ - Broker cancelled a consumer of ours. - This is also generated in response to cancelling consumption from a queue - """ - - BROKER_CANCEL = 0 - REFUSED_ON_RECONNECT = 1 - USER_CANCEL = 2 - - def __init__(self, queue, reason): - """ - :param queue: Queue whose consumer was cancelled - :param reason: Reason why the consumer was cancelled - ConsumerCancelled.BROKER_CANCEL - broker informed us about cancelling - ConsumerCancelled.REFUSED_ON_RECONNECT - during a reconnect, I tried to consume an exclusive - queue and got ACCESS_REFUSED. - These messages will arrive between ConnectionDown and - ConnectionUp. - ConsumedCancelled.USER_CANCEL - user called cluster.cancel() - """ - self.queue = queue - self.reason = reason diff --git a/coolamqp/exceptions.py b/coolamqp/exceptions.py new file mode 100644 index 0000000000000000000000000000000000000000..f4ba6aa34585e02e437d65e82aa130bbdbf58908 --- /dev/null +++ b/coolamqp/exceptions.py @@ -0,0 +1,51 @@ +# coding=UTF-8 +from __future__ import absolute_import, division, print_function + + +class CoolAMQPError(Exception): + """Base class for CoolAMQP errors""" + + + +class ConsumerError(CoolAMQPError): + """ + Exceptions passed to consumer callables. + """ + + +class UplinkLost(ConsumerError): + """ + Uplink to the network has been lost, I am working on regaining connectivity + right now. + """ + +class ConsumerCancelled(CoolAMQPError): + """ + The consumer has been cancelled + """ + + +class AMQPError(CoolAMQPError): + """ + Base class for errors received from AMQP server + """ + def __init__(self, *args): + """ + + :param args: can be either reply_code, reply_text, class_id, method_id + or a ConnectionClose/ChannelClose. + """ + from coolamqp.framing.definitions import ConnectionClose, ChannelClose + + if isinstance(args[0], (ConnectionClose, ChannelClose)): + self.reply_code = args[0].reply_code + self.reply_text = args[0].reply_text + self.class_id = args[0].class_id + self.method_id = args[0].method_id + else: + assert len(args) == 4 + self.reply_code, self.reply_text, self.class_id, self.method_id = args + + +class ResourceLocked(AMQPError): + """Shorthand to catch that stuff easier""" diff --git a/coolamqp/framing/__init__.py b/coolamqp/framing/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..7e413d8fc8f284c7d85e772ebf519058ee1af9b8 --- /dev/null +++ b/coolamqp/framing/__init__.py @@ -0,0 +1,10 @@ +# coding=UTF-8 +from __future__ import absolute_import, division, print_function + +""" +Definitions of framing. +Mechanisms for serialization/deserialization of AMQP framing and other types. + + +definitions.py is machine-generated from AMQP specification. +""" \ No newline at end of file diff --git a/coolamqp/framing/base.py b/coolamqp/framing/base.py new file mode 100644 index 0000000000000000000000000000000000000000..3ee0f7a13b4f537482d26cd7a3fdca3ad69ac6cc --- /dev/null +++ b/coolamqp/framing/base.py @@ -0,0 +1,181 @@ +# coding=UTF-8 +from __future__ import absolute_import, division, print_function + +import logging +import struct + +logger = logging.getLogger(__name__) + + +AMQP_HELLO_HEADER = b'AMQP\x00\x00\x09\x01' + + +# name => (length|None, struct ID|None, reserved-field-value : for struct if structable, bytes else, length of default) +BASIC_TYPES = {u'bit': (None, None, "0", None), # special case + u'octet': (1, 'B', "b'\\x00'", 1), + u'short': (2, 'H', "b'\\x00\\x00'", 2), + u'long': (4, 'I', "b'\\x00\\x00\\x00\\x00'", 4), + u'longlong': (8, 'Q', "b'\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00'", 8), + u'timestamp': (8, 'Q', "b'\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00'", 8), + u'table': (None, None, "b'\\x00\\x00\\x00\\x00'", 4), # special case + u'longstr': (None, None, "b'\\x00\\x00\\x00\\x00'", 4), # special case + u'shortstr': (None, None, "b'\\x00'", 1), # special case + } + +DYNAMIC_BASIC_TYPES = (u'table', u'longstr', u'shortstr') + + +class AMQPFrame(object): # base class for framing + FRAME_TYPE = None # override me! + + def __init__(self, channel): + self.channel = channel + + def write_to(self, buf): + """ + Write a complete frame to buffer + + This writes type and channel ID. + """ + # DO NOT UNCOMMENT buf.write(struct.pack('!BH', self.FRAME_TYPE, self.channel)) + raise NotImplementedError('Please write the frame type and channel in child classes, its faster that way ') + + @staticmethod + def unserialize(channel, payload_as_buffer): + """ + Unserialize from a buffer. + Buffer starts at frame's own payload - type, channel and size was already obtained. + Payload does not contain FRAME_EMD. + AMQPHeartbeatFrame does not have to implement this. + """ + raise NotImplementedError('Override me') + + def get_size(self): + """ + Return size of this frame, in bytes, from frame type to frame_end + :return: int + """ + raise NotImplementedError('Override me') + + +class AMQPPayload(object): + """Payload is something that can write itself to bytes, + or at least provide a buffer to do it.""" + + def write_to(self, buf): + """ + Emit itself into a buffer, from length to FRAME_END + + :param buf: buffer to write to (will be written using .write) + """ + + def get_size(self): + """ + Return size of this payload + :return: int + """ + raise NotImplementedError() + + +class AMQPClass(object): + """An AMQP class""" + + +class AMQPContentPropertyList(object): + """ + A class is intmately bound with content and content properties + """ + PROPERTIES = [] + + @staticmethod + def zero_property_flags(property_flags): + """ + Given a binary property_flags, set all bit properties to 0. + + This leaves us with a canonical representation, that can be used + in obtaining a particular property list + :param property_flags: binary + :return: binary + """ + # this is a default implementation. + # compiler should emit it's own when the content property list has a + # possible bit field + return property_flags + + def write_to(self, buf): + """Serialize itself (flags + values) to a buffer""" + raise Exception('This is an abstract method') + + @staticmethod + def from_buffer(self, buf, start_offset): + """ + Return an instance of self, loaded from a buffer. + + This does not have to return length, because it is always passed exactly enough of a buffer. + + Buffer HAS TO start at property_flags + """ + raise Exception('This is an abstract method') + + def get_size(self): + """ + How long is property_flags + property_values + :return: int + """ + raise Exception('This is an abstract method') + + +class AMQPMethodPayload(AMQPPayload): + RESPONSE_TO = None + REPLY_WITH = [] + FIELDS = [] + + def write_to(self, buf): + """ + Write own content to target buffer - starting from LENGTH, ending on FRAME_END + :param buf: target buffer + """ + from coolamqp.framing.definitions import FRAME_END + + if self.IS_CONTENT_STATIC: + buf.write(self.STATIC_CONTENT) + else: + buf.write(struct.pack('!I', self.get_size()+2)) + buf.write(self.BINARY_HEADER) + self.write_arguments(buf) + buf.write(chr(FRAME_END)) + + def get_size(self): + """ + Calculate the size of this frame. + + :return: int, size of argument section + """ + if self.IS_CONTENT_STATIC: + return len(self.STATIC_CONTENT)-4-4-1 # minus length, class, method, frame_end + + raise NotImplementedError() + + def write_arguments(self, buf): + """ + Write the argument portion of this frame into buffer. + + :param buf: buffer to write to + :return: how many bytes written + :raise ValueError: some field here is invalid! + """ + raise NotImplementedError() + + @staticmethod + def from_buffer(buf, offset): + """ + Construct this frame from a buffer + + :param buf: a buffer to construct the frame from + :type buf: buffer or memoryview + :param offset: offset the argument portion begins at + :type offset: int + :return: tuple of (an instance of %s, amount of bytes consumed as int) + :raise ValueError: invalid data + """ + raise NotImplementedError('') diff --git a/coolamqp/framing/compilation/__init__.py b/coolamqp/framing/compilation/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c932491a7c750218a3d62edb6656415c22a09be1 --- /dev/null +++ b/coolamqp/framing/compilation/__init__.py @@ -0,0 +1,5 @@ +# coding=UTF-8 +from __future__ import absolute_import, division, print_function +""" +Function that compiles amqp0-9-1.xml to definitions.py +""" \ No newline at end of file diff --git a/coolamqp/framing/compilation/compile_definitions.py b/coolamqp/framing/compilation/compile_definitions.py new file mode 100644 index 0000000000000000000000000000000000000000..ec0022e019e19b7d7dddef07abb0773b4b16a95c --- /dev/null +++ b/coolamqp/framing/compilation/compile_definitions.py @@ -0,0 +1,467 @@ +from __future__ import division + +import collections +import math +import struct +from xml.etree import ElementTree + +import six +from coolamqp.framing.base import BASIC_TYPES + +from coolamqp.framing.compilation.utilities import get_constants, get_classes, get_domains, \ + name_class, format_method_class_name, format_field_name, ffmt, to_docstring, pythonify_name, to_code_binary, \ + frepr, get_size + +TYPE_TRANSLATOR = { + 'shortstr': 'binary type (max length 255)', + 'longstr': 'binary type', + 'table': 'table. See coolamqp.uplink.framing.field_table', + 'bit': 'bool', + 'octet': 'int, 8 bit unsigned', + 'short': 'int, 16 bit unsigned', + 'long': 'int, 32 bit unsigned', + 'longlong': 'int, 64 bit unsigned', + 'timestamp': '64 bit signed POSIX timestamp (in seconds)', +} + +def compile_definitions(xml_file='resources/amqp0-9-1.extended.xml', out_file='coolamqp/framing/definitions.py'): + """parse resources/amqp-0-9-1.xml into """ + + xml = ElementTree.parse(xml_file) + out = open(out_file, 'wb') + + out.write('''# coding=UTF-8 +from __future__ import print_function, absolute_import +""" +A Python version of the AMQP machine-readable specification. + +Generated automatically by CoolAMQP from AMQP machine-readable specification. +See coolamqp.uplink.framing.compilation for the tool + +AMQP is copyright (c) 2016 OASIS +CoolAMQP is copyright (c) 2016 DMS Serwis s.c. +""" + +import struct, collections, warnings, logging, six + +from coolamqp.framing.base import AMQPClass, AMQPMethodPayload, AMQPContentPropertyList +from coolamqp.framing.field_table import enframe_table, deframe_table, frame_table_size +from coolamqp.framing.compilation.content_property import compile_particular_content_property_list_class + +logger = logging.getLogger(__name__) + +Field = collections.namedtuple('Field', ('name', 'type', 'basic_type', 'reserved')) + +''') + + def line(data, *args, **kwargs): + out.write(ffmt(data, *args, sane=True)) + + # Output core ones + FRAME_END = None + con_classes = collections.defaultdict(list) + line('# Core constants\n') + for constant in get_constants(xml): + if pythonify_name(constant.name) == 'FRAME_END': + FRAME_END = constant.value + g = ffmt('%s = %s', pythonify_name(constant.name), constant.value) + line(g) + if constant.docs: + lines = constant.docs.split('\n') + line(' # %s\n', lines[0]) + if len(lines) > 1: + for ln in lines[1:]: + line(u' '*len(g)) + line(u' # %s\n', ln) + else: + line('\n') + + if constant.kind: + con_classes[constant.kind].append(pythonify_name(constant.name)) + + for constant_kind, constants in con_classes.items(): + line('\n%s = [%s]', pythonify_name(constant_kind), u', '.join(constants)) + + # get domains + domain_to_basic_type = {} + line('\n\n\nDOMAIN_TO_BASIC_TYPE = {\n') + for domain in get_domains(xml): + line(u' %s: %s,\n', frepr(domain.name), frepr(None if domain.elementary else domain.type)) + domain_to_basic_type[domain.name] = domain.type + + line('}\n') + + class_id_to_contentpropertylist = {} + + # below are stored as strings! + methods_that_are_reply_reasons_for = {} # eg. ConnectionOpenOk: ConnectionOk + methods_that_are_replies_for = {} # eg. ConnectionOk: [ConnectionOpenOk] + + # Output classes + for cls in get_classes(xml): + + cls = cls._replace(properties=[p._replace(basic_type=domain_to_basic_type[p.type]) for p in cls.properties]) + + line('''\nclass %s(AMQPClass): + """ + %s + """ + NAME = %s + INDEX = %s + +''', + name_class(cls.name), to_docstring(None, cls.docs), frepr(cls.name), cls.index) + + if len(cls.properties) > 0: + class_id_to_contentpropertylist[cls.index] = name_class(cls.name)+'ContentPropertyList' + + line('''\nclass %sContentPropertyList(AMQPContentPropertyList): + """ + %s + """ + FIELDS = [ +''', + + name_class(cls.name), to_docstring(None, cls.docs), frepr(cls.name), cls.index, name_class(cls.name)) + + is_static = all(property.basic_type not in ('table', 'longstr', 'shortstr') for property in cls.properties) + + for property in cls.properties: + if property.basic_type == 'bit': + raise ValueError('bit properties are not supported!' + ) + line(' Field(%s, %s, %s, %s),\n', frepr(property.name), frepr(property.type), frepr(property.basic_type), repr(property.reserved)) + line(''' ] + # A dictionary from a zero property list to a class typized with + # some fields + PARTICULAR_CLASSES = {} +\n''', + name_class(cls.name)) + + if any(prop.basic_type == 'bit' for prop in cls.properties): + raise NotImplementedError('I should emit a custom zero_property_list staticmethod :(') + line(u''' def __new__(self, **kwargs): + """ + Return a property list. +''') + property_strs = [] + + my_props = [prop for prop in cls.properties if (not prop.reserved)] + for property in my_props: + line(' :param %s: %s\n', format_field_name(property.name), property.label) + line(' :type %s: %s (AMQP as %s)\n', format_field_name(property.name), TYPE_TRANSLATOR[property.basic_type], property.basic_type) + line(' """\n') + zpf_len = int(math.ceil(len(cls.properties) // 15)) + + first_byte = True # in 2-byte group + piece_index = 7 # from 7 downto 0 + fields_remaining = len(cls.properties) + + byte_chunk = [] + line(u' zpf = bytearray([\n') + + for field in cls.properties: + # a bit + if piece_index > 0: + if field.reserved or field.basic_type == 'bit': + pass # zero anyway + else: + byte_chunk.append(u"(('%s' in kwargs) << %s)" % (format_field_name(field.name), piece_index)) + piece_index -= 1 + else: + if first_byte: + if field.reserved or field.basic_type == 'bit': + pass # zero anyway + else: + byte_chunk.append(u"int('%s' in kwargs)" % (format_field_name(field.name),)) + else: + # this is the "do we need moar flags" section + byte_chunk.append(u"kwargs['%s']" % ( + int(fields_remaining > 1) + )) + + # Emit the byte + line(u' %s,\n', u' | '.join(byte_chunk)) + byte_chunk = [] + first_byte = not first_byte + piece_index = 7 + fields_remaining -= 1 + + if len(byte_chunk) > 0: + line(u' %s\n', u' | '.join(byte_chunk)) # We did not finish + + line(u' ])\n zpf = six.binary_type(zpf)\n') + line(u''' + if zpf in %s.PARTICULAR_CLASSES: + warnings.warn(u"""You could go faster. + + If you know in advance what properties you will be using, use typized constructors like + + # runs once + my_type = BasicContentPropertyList.typize('content_type', 'content_encoding') + # runs many times + props = my_type('text/plain', 'utf8') + + instead of + + # runs many times + props = BasicContentPropertyList(content_type='text/plain', content_encoding='utf8') + + This way you will be faster. + + If you do not know in advance what properties you will be using, it is correct to use + this constructor. + """) + + return %s.PARTICULAR_CLASSES[zpf](**kwargs) + else: + logger.debug('Property field (%s:%d) not seen yet, compiling', repr(zpf)) + c = compile_particular_content_property_list_class(zpf, %s.FIELDS) + %s.PARTICULAR_CLASSES[zpf] = c + return c(**kwargs) +'''.replace('%s', name_class(cls.name) + 'ContentPropertyList').replace('%d', '%s')) + + line(u''' + @staticmethod + def typize(*fields): + ''') + line(u' zpf = bytearray([\n') + + first_byte = True # in 2-byte group + piece_index = 7 # from 7 downto 0 + fields_remaining = len(cls.properties) + byte_chunk = [] + + for field in cls.properties: + # a bit + if piece_index > 0: + if field.reserved or field.basic_type == 'bit': + pass # zero + else: + byte_chunk.append(u"(('%s' in fields) << %s)" % (format_field_name(field.name), piece_index)) + piece_index -= 1 + else: + if first_byte: + if field.reserved or field.basic_type == 'bit': + pass #zero + else: + byte_chunk.append(u"int('%s' in kwargs)" % (format_field_name(field.name),)) + else: + # this is the "do we need moar flags" section + byte_chunk.append(u"kwargs['%s']" % ( + int(fields_remaining > 1) + )) + + # Emit the byte + line(u' %s,\n', u' | '.join(byte_chunk)) + byte_chunk = [] + first_byte = not first_byte + piece_index = 7 + fields_remaining -= 1 + + if len(byte_chunk) > 0: + line(u' %s\n', u' | '.join(byte_chunk)) # We did not finish + + line(u''' ]) + zpf = six.binary_type(zpf) + if zpf in %s.PARTICULAR_CLASSES: + return %s.PARTICULAR_CLASSES[zpf] + else: + logger.debug('Property field (%s:%d) not seen yet, compiling', repr(zpf)) + c = compile_particular_content_property_list_class(zpf, %s.FIELDS) + %s.PARTICULAR_CLASSES[zpf] = c + return c +'''.replace("%s", name_class(cls.name) + 'ContentPropertyList').replace('%d', '%s')) + + line(u''' + @staticmethod + def from_buffer(buf, offset): + """ + Return a content property list instance unserialized from + buffer, so that buf[offset] marks the start of property flags + """ + # extract property flags + pfl = 2 + while ord(buf[offset + pfl - 1]) & 1: + pfl += 2 + zpf = %s.zero_property_flags(buf[offset:offset+pfl]) + if zpf in %s.PARTICULAR_CLASSES: + return %s.PARTICULAR_CLASSES[zpf].from_buffer(buf, offset) + else: + logger.debug('Property field (%s:%d) not seen yet, compiling', repr(zpf)) + c = compile_particular_content_property_list_class(zpf, %s.FIELDS) + %s.PARTICULAR_CLASSES[zpf] = c + return c.from_buffer(buf, offset) + +'''.replace('%s', name_class(cls.name) + 'ContentPropertyList').replace("%d", "%s")) + + # ============================================ Do methods for this class + for method in cls.methods: + full_class_name = '%s%s' % (name_class(cls.name), format_method_class_name(method.name)) + + # annotate types + method.fields = [field._replace(basic_type=domain_to_basic_type[field.type]) for field in method.fields] + + non_reserved_fields = [field for field in method.fields if not field.reserved] + + is_static = method.is_static() + if is_static: + static_size = get_size(method.fields) + + is_content_static = len([f for f in method.fields if not f.reserved]) == 0 + + if len(non_reserved_fields) == 0: + slots = u'' + else: + slots = (u', '.join(map(lambda f: frepr(format_field_name(f.name)), non_reserved_fields)))+u', ' + + line('''\nclass %s(AMQPMethodPayload): + """ + %s + """ + __slots__ = (%s) + + NAME = %s + + INDEX = (%s, %s) # (Class ID, Method ID) + BINARY_HEADER = %s # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = %s, %s + + IS_SIZE_STATIC = %s # this means that argument part has always the same length + IS_CONTENT_STATIC = %s # this means that argument part has always the same content +''', + + full_class_name, + to_docstring(method.label, method.docs), + slots, + frepr(cls.name + '.' + method.name), + frepr(cls.index), frepr(method.index), + to_code_binary(struct.pack("!HH", cls.index, method.index)), + repr(method.sent_by_client), + repr(method.sent_by_server), + repr(is_static), + repr(is_content_static) + ) + + _namify = lambda x: name_class(cls.name) + format_method_class_name(x) + + methods_that_are_replies_for[full_class_name] = [] + for response in method.response: + methods_that_are_reply_reasons_for[_namify(response)] = full_class_name + methods_that_are_replies_for[full_class_name].append(_namify(response)) + + if is_content_static: + + line(''' STATIC_CONTENT = %s # spans LENGTH, CLASS ID, METHOD ID, ....., FRAME_END +''', + to_code_binary(struct.pack('!LHH', static_size + 4, cls.index, method.index) + \ + method.get_static_body() + \ + struct.pack('!B', FRAME_END))) + + # fields + if len(method.fields) > 0: + line('\n # See constructor pydoc for details\n') + line(' FIELDS = [ \n') + + for field in method.fields: + line(' Field(%s, %s, %s, reserved=%s),\n', frepr(field.name), frepr(field.type), frepr(field.basic_type), repr(field.reserved)) + + line(' ]\n') + + + + # constructor + line('''\n def __init__(%s): + """ + Create frame %s +''', + u', '.join(['self'] + [format_field_name(field.name) for field in non_reserved_fields]), + cls.name + '.' + method.name, + ) + + if len(non_reserved_fields) > 0: + line('\n') + + for field in non_reserved_fields: + if (field.label is not None) or (field.docs is not None): + line(' :param %s: %s\n', format_field_name(field.name), + to_docstring(field.label, field.docs, prefix=12, blank=False)) + + + + line(' :type %s: %s (%s in AMQP)\n', format_field_name(field.name), TYPE_TRANSLATOR[field.basic_type], field.type) + + line(' """\n') + + for field in non_reserved_fields: + line(' self.%s = %s\n', format_field_name(field.name), format_field_name(field.name)) + + if len(non_reserved_fields) == 0: + line('\n') + + # end + if not is_content_static: + from coolamqp.framing.compilation.textcode_fields import get_serializer, get_counter, get_from_buffer + line('\n def write_arguments(self, buf):\n') + line(get_serializer(method.fields, 'self.', 2)) + + line(' def get_size(self):\n') + line(get_counter(method.fields, 'self.', 2)) + + line('''\n @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset +''') + + line(get_from_buffer(method.fields, '', 2, remark=(method.name == 'deliver'))) + line(" return %s(%s)", + full_class_name, + u', '.join(format_field_name(field.name) for field in method.fields if not field.reserved)) + + line('\n\n') + + # Get me a dict - (classid, methodid) => class of method + dct = {} + for cls in get_classes(xml): + for method in cls.methods: + dct[((cls.index, method.index))] = '%s%s' % (name_class(cls.name), format_method_class_name(method.name)) + + line('\nIDENT_TO_METHOD = {\n') + for k, v in dct.items(): + line(' %s: %s,\n', repr(k), v) + line('}\n\n') + + line('\nBINARY_HEADER_TO_METHOD = {\n') + for k, v in dct.items(): + line(' %s: %s,\n', to_code_binary(struct.pack('!HH', *k)), v) + line('}\n\n') + + line('\nCLASS_ID_TO_CONTENT_PROPERTY_LIST = {\n') + for k,v in class_id_to_contentpropertylist.items(): + line(' %s: %s,\n', k, v) + line('}\n\n') + + line(u'''# Methods that are sent as replies to other methods, ie. ConnectionOpenOk: ConnectionOpen +# if a method is NOT a reply, it will not be in this dict +# a method may be a reply for AT MOST one method +REPLY_REASONS_FOR = {\n''') + for k,v in methods_that_are_reply_reasons_for.items(): + line(u' %s: %s,\n' % (k, v)) + + line(u'''} + +# Methods that are replies for other, ie. ConnectionOpenOk: ConnectionOpen +# a method may be a reply for ONE or NONE other methods +# if a method has no replies, it will have an empty list as value here +REPLIES_FOR= {\n''') + + for k,v in methods_that_are_replies_for.items(): + line(u' %s: [%s],\n' % (k, u', '.join(map(str, v)))) + line(u'}\n') + + out.close() + + +if __name__ == '__main__': + compile_definitions() diff --git a/coolamqp/framing/compilation/content_property.py b/coolamqp/framing/compilation/content_property.py new file mode 100644 index 0000000000000000000000000000000000000000..73732e443079dd9fa83f59b1d7679ea3f2ef1c36 --- /dev/null +++ b/coolamqp/framing/compilation/content_property.py @@ -0,0 +1,122 @@ +# coding=UTF-8 +from __future__ import absolute_import, division, print_function +"""Generate serializers/unserializers/length getters for given property_flags""" +import six +import struct +import logging +from coolamqp.framing.compilation.textcode_fields import get_counter, get_from_buffer, get_serializer +from coolamqp.framing.base import AMQPContentPropertyList +from coolamqp.framing.field_table import enframe_table, deframe_table, frame_table_size + + +logger = logging.getLogger(__name__) + + +def _compile_particular_content_property_list_class(zpf, fields): + """ + Compile a particular content property list. + + Particularity stems from + :param zpf: zero property list, as bytearray + :param fields: list of all possible fields in this content property + """ + from coolamqp.framing.compilation.utilities import format_field_name + + if any(field.basic_type == 'bit' for field in fields): + return u"raise NotImplementedError('I don't support bits in properties yet')" + + # Convert ZPF to a list of [if_exists::bool] + even = True + zpf_bits = [] + for q in bytearray(zpf): + p = bin(q)[2:] + p = (u'0' * (8 - len(p))) + p + + if not even: + p = p[:7] + + zpf_bits.extend(map(lambda x: bool(int(x)), p)) + + zpf_length = len(zpf) + + # 1 here does not mean that field is present. All bit fields are present, but 0 in a ZPF. Fix this. + zpf_bits = [zpf_bit or field.type == 'bit' for zpf_bit, field in zip(zpf_bits, fields)] + + mod = [u'''class ParticularContentTypeList(AMQPContentPropertyList): + """ + For fields: +'''] + + for field in fields: + mod.append(u' * %s::%s' % (format_field_name(field.name), field.type)) + if field.reserved: + mod.append(u' (reserved)') + mod.append(u'\n') + + x = repr(six.binary_type(zpf)) + if not x.startswith('b'): + x = 'b'+x + + present_fields = [field for field, present in zip(fields, zpf_bits) if present] + + mod.append(u''' + """ +''') + + if len(present_fields) == 0: + slots = u'' + else: + slots = (u', '.join((u"u'%s'" % format_field_name(field.name) for field in present_fields)))+u', ' + + mod.append(u''' + __slots__ = (%s) +''' % slots) + + mod.append(u''' + # A value for property flags that is used, assuming all bit fields are FALSE (0) + ZERO_PROPERTY_FLAGS = %s +''' % (x, )) + + if len(present_fields) > 0: + mod.append(u''' + def __init__(self, %s): +''' % (u', '.join(format_field_name(field.name) for field in present_fields))) + + for field in present_fields: + mod.append(u' self.%s = %s\n'.replace(u'%s', format_field_name(field.name))) + + # Let's do write_to + mod.append(u'\n def write_to(self, buf):\n') + mod.append(u' buf.write(') + repred_zpf = repr(zpf) + if not zpf.startswith('b'): + repred_zpf = 'b' + repred_zpf + mod.append(repred_zpf) + mod.append(u')\n') + + mod.append(get_serializer(present_fields, prefix='self.', indent_level=2)) + + # from_buffer + # note that non-bit values + mod.append(u' @classmethod\n') + mod.append(u' def from_buffer(cls, buf, start_offset):\n offset = start_offset + %s\n' % (zpf_length, )) + mod.append(get_from_buffer( + present_fields + , prefix='', indent_level=2)) + mod.append(u' return cls(%s)\n' % + u', '.join(format_field_name(field.name) for field in present_fields)) + + + # get_size + mod.append(u'\n def get_size(self):\n') + mod.append(get_counter(present_fields, prefix='self.', indent_level=2)[:-1]) # skip eol + mod.append(u' + %s\n' % (zpf_length, )) # account for pf length + + return u''.join(mod) + + +def compile_particular_content_property_list_class(zpf, fields): + q = _compile_particular_content_property_list_class(zpf, fields) + logger.debug('Compiling\n%s', q) + exec(q) + return ParticularContentTypeList diff --git a/coolamqp/framing/compilation/textcode_fields.py b/coolamqp/framing/compilation/textcode_fields.py new file mode 100644 index 0000000000000000000000000000000000000000..3a1722031e875d33ce39bb86cfc4dd9ba9ec85a0 --- /dev/null +++ b/coolamqp/framing/compilation/textcode_fields.py @@ -0,0 +1,252 @@ +# coding=UTF-8 +""" +Return Python code used to serialize/unserialize/get_size of lists of fields. + +If you are going to paste the code you get here, note you nede to paste it into +a module that has following ok: + + * local variables denoted with a list of Field (namedtuple) with optional prefix exist + * function header was already emitted + * indent_level is in multiple of fours + * following imports exists: + * import struct + * from coolamqp.framing.field_table import enframe_table, deframe_table, frame_table_size + * local variables buf and offset exist + * local variable start_offset can be created + +""" +from __future__ import absolute_import, division, print_function +import math + +from coolamqp.framing.base import BASIC_TYPES, DYNAMIC_BASIC_TYPES +from coolamqp.framing.compilation.utilities import format_field_name, get_size + +def get_counter(fields, prefix='', indent_level=2): + """ + Emit code that counts how long this struct is. + + :param fields: list of Field instances + :param prefix: pass "self." is inside a class + :param indent_level: amount of tabs + :return: block of code that does that + """ + + parts = [] + accumulator = 0 + bits = 0 + for field in fields: + bt = field.basic_type + nam = prefix+format_field_name(field.name) + + if (bits > 0) and (bt != 'bit'): # sync bits if not + accumulator += int(math.ceil(bits / 8)) + bits = 0 + + if field.basic_type == 'bit': + bits += 1 + elif field.reserved: + accumulator += BASIC_TYPES[field.basic_type][3] + elif BASIC_TYPES[bt][0] is not None: + accumulator += BASIC_TYPES[field.basic_type][0] + elif bt == 'shortstr': + parts.append('len('+nam+')') + accumulator += 1 + elif bt == 'longstr': + parts.append('len(' + nam + ')') + accumulator += 4 + elif bt == 'table': + parts.append('frame_table_size(' + nam + ')') + accumulator += 0 # because frame_table_size accounts for that 4 leading bytes + else: + raise Exception() + + if bits > 0: # sync bits + accumulator += int(math.ceil(bits / 8)) + + return (u' '*indent_level)+u'return '+(u' + '.join([str(accumulator)]+parts))+u'\n' + + +def get_from_buffer(fields, prefix='', indent_level=2, remark=False): + """ + Emit code that collects values from buf:offset, updating offset as progressing. + :param remark: BE FUCKING VERBOSE! #DEBUG + """ + code = [] + def emit(fmt, *args): + args = list(args) + code.append(u' '*indent_level) + assert fmt.count('%s') == len(args) + for arg in args: + fmt = fmt.replace('%s', str(arg), 1) + code.append(fmt) + code.append('\n') + + + # actually go and load it + + bits = [] + ln = {'ln': 0} # so I can modify from outside + to_struct = [] + + def emit_bits(): + if len(bits) == 0: + return + if remark: + print('Bits are being banged') + if all(n == '_' for n in bits): + # everything is reserved, lol + emit('offset += 1') + else: + to_struct.append(('_bit', 'B')) + emit_structures(dont_do_bits=True) + + for multiplier, bit in enumerate(bits): + if bit != '_': + emit("%s = bool(_bit >> %s)", bit, multiplier) + emit('offset += 1') + + del bits[:] + + def emit_structures(dont_do_bits=False): + if not dont_do_bits: + emit_bits() + if len(to_struct) == 0: + return + fffnames = [a for a, b in to_struct if a != u'_'] # skip reserved + ffffmts = [b for a, b in to_struct] + emit("%s, = struct.unpack_from('!%s', buf, offset)", u', '.join(fffnames), u''.join(ffffmts)) + emit("offset += %s", ln['ln']) + ln['ln'] = 0 + del to_struct[:] + + for field in fields: + fieldname = prefix+format_field_name(field.name) + + if (len(bits) > 0) and (field.basic_type != u'bit'): + emit_bits() + + if remark: + print('Doing', fieldname, 'of type', field.basic_type) + + # offset is current start + # length is length to read + if BASIC_TYPES[field.basic_type][0] is not None: + # static type shit has + + assert len(bits) == 0 + + if field.reserved: + to_struct.append((u'_', '%sx' % (BASIC_TYPES[field.basic_type][0],))) + else: + to_struct.append((fieldname, BASIC_TYPES[field.basic_type][1])) + + ln['ln'] += BASIC_TYPES[field.basic_type][0] + elif field.basic_type == u'bit': + bits.append('_' if field.reserved else fieldname) + elif field.basic_type == u'table': # oh my god + emit_structures() + + assert len(bits) == 0 + assert len(to_struct) == 0 + + emit("%s, delta = deframe_table(buf, offset)", fieldname) + emit("offset += delta") + else: # longstr or shortstr + f_q, f_l = ('L', 4) if field.basic_type == u'longstr' else ('B', 1) + to_struct.append(('s_len', f_q)) + ln['ln'] += f_l + emit_structures() + if field.reserved: + emit("offset += s_len # reserved field!") + else: + emit("%s = buf[offset:offset+s_len]", fieldname) + emit("offset += s_len") + + # check bits for overflow + if len(bits) == 8: + emit_bits() + + emit_structures() + + return u''.join(code) + + +def get_serializer(fields, prefix='', indent_level=2): + """ + Emit code that serializes the fields into buf at offset + + :param fields: list of Field instances + :param prefix: pass "self." is inside a class + :return: block of code that does that + """ + code = [] + + def emit(fmt, *args): + args = list(args) + code.append(u' '*indent_level) + while len(args) > 0: + fmt = fmt.replace('%s', args[0], 1) + del args[0] + code.append(fmt) + code.append('\n') + + formats = [] + format_args = [] + bits = [] + + def emit_bits(): + p = [] + formats.append('B') + if all(bit_name == 'False' for bit_name in bits): + format_args.append('0') + else: + for bit_name, modif in zip(bits, range(8)): + if bit_name != 'False': + p.append('('+bit_name+' << %s)' % (modif, )) # yes you can << bools + format_args.append(u' | '.join(p)) + del bits[:] + + def emit_single_struct_pack(): + emit("buf.write(struct.pack('!%s', %s))", u''.join(formats), u', '.join(format_args)) + del formats[:] + del format_args[:] + + for field in fields: + nam = prefix+format_field_name(field.name) + + if (len(bits) == 8) or ((len(bits) > 0) and field.basic_type != 'bit'): + emit_bits() + + if field.basic_type == 'bit': + if field.reserved: + bits.append("False") + else: + bits.append(nam) + elif field.reserved: + # Just pasta + emit('buf.write(%s)', BASIC_TYPES[field.basic_type][2]) + else: + if field.basic_type in ('shortstr', 'longstr'): + formats.append('B' if field.basic_type == 'shortstr' else 'I') + format_args.append('len('+nam+')') + emit_single_struct_pack() + emit('buf.write(%s)', nam) + elif field.basic_type == 'table': + if len(bits) > 0: + emit_bits() + if len(formats) > 0: + emit_single_struct_pack() + emit('enframe_table(buf, %s)', nam) + else: + formats.append(BASIC_TYPES[field.basic_type][1]) + format_args.append(nam) + + if len(bits) > 0: + emit_bits() + if len(formats) > 0: + emit_single_struct_pack() + + emit('') # eol + + return u''.join(code) + diff --git a/coolamqp/framing/compilation/utilities.py b/coolamqp/framing/compilation/utilities.py new file mode 100644 index 0000000000000000000000000000000000000000..2504eab9b23dcf1f20d5889bbfafb47e62d884df --- /dev/null +++ b/coolamqp/framing/compilation/utilities.py @@ -0,0 +1,222 @@ +# coding=UTF-8 +from __future__ import absolute_import, division, print_function + +import math +from collections import namedtuple + +import six + +from coolamqp.framing.base import BASIC_TYPES, DYNAMIC_BASIC_TYPES + +# docs may be None + + + +Constant = namedtuple('Constant', ('name', 'value', 'kind', 'docs')) # kind is AMQP constant class # value is int +Field = namedtuple('Field', ('name', 'type', 'label', 'docs', 'reserved', 'basic_type')) # reserved is bool +Method = namedtuple('Method', ('name', 'synchronous', 'index', 'label', 'docs', 'fields', 'response', + 'sent_by_client', 'sent_by_server', 'constant')) + # synchronous is bool, constant is bool + # repponse is a list of method.name +Class_ = namedtuple('Class_', ('name', 'index', 'docs', 'methods', 'properties')) # label is int +Domain = namedtuple('Domain', ('name', 'type', 'elementary')) # elementary is bool + + +class Method(object): + def __init__(self, name, synchronous, index, label, docs, fields, response, sent_by_client, sent_by_server): + self.name = name + self.synchronous = synchronous + self.index = index + self.fields = fields + self.response = response + self.label = label + self.docs = docs + self.sent_by_client = sent_by_client + self.sent_by_server = sent_by_server + + self.constant = len([f for f in self.fields if not f.reserved]) == 0 + + def get_static_body(self): # only arguments part + body = [] + bits = 0 + for field in self.fields: + + if bits > 0 and field.basic_type != 'bit': + body.append(b'\x00' * math.ceil(bits / 8)) + bits = 0 + + if field.basic_type == 'bit': + bits += 1 + else: + body.append(eval(BASIC_TYPES[field.basic_type][2])) + return b''.join(body) + + def is_static(self, domain_to_type=None): # is size constant? + for field in self.fields: + if field.basic_type in DYNAMIC_BASIC_TYPES: + return False + return True + + +def get_size(fields): # assume all fields have static length + size = 0 + bits = 0 + for field in fields: + + if (bits > 0) and (field.basic_type != 'bit'): # sync bits + size += int(math.ceil(bits / 8)) + bits = 0 + + if BASIC_TYPES[field.basic_type][0] is None: + if field.basic_type == 'bit': + bits += 1 + else: + size += len(BASIC_TYPES[field.basic_type][2]) # default minimum entry + else: + size += BASIC_TYPES[field.basic_type][0] + + if bits > 0: # sync bits + size += int(math.ceil(bits / 8)) + + return size + + +def get_docs(elem): + for kid in elem.getchildren(): + + if kid.tag == 'rule': + return get_docs(kid) + + s = kid.text.strip().split('\n') + return u'\n'.join([u.strip() for u in s if len(u.strip()) > 0]) + + return None + + +def for_domain(elem): + a = elem.attrib + return Domain(six.text_type(a['name']), a['type'], a['type'] == a['name']) + + +def for_field(elem): # for <field> in <method> + a = elem.attrib + return Field(six.text_type(a['name']), a['domain'] if 'domain' in a else a['type'], + a.get('label', None), + get_docs(elem), + a.get('reserved', '0') == '1', + None) + +def for_method(elem): # for <method> + a = elem.attrib + return Method(six.text_type(a['name']), bool(int(a.get('synchronous', '0'))), int(a['index']), a.get('label', None), get_docs(elem), + [for_field(fie) for fie in elem.getchildren() if fie.tag == 'field'], + [e.attrib['name'] for e in elem.findall('response')], + # if chassis=server that means server has to accept it + any([e.attrib.get('name', '') == 'server' for e in elem.getchildren() if e.tag == 'chassis']), + any([e.attrib.get('name', '') == 'client' for e in elem.getchildren() if e.tag == 'chassis']) + ) + +def for_class(elem): # for <class> + a = elem.attrib + methods = sorted([for_method(me) for me in elem.getchildren() if me.tag == 'method'], key=lambda m: (m.name.strip('-')[0], -len(m.response))) + return Class_(six.text_type(a['name']), int(a['index']), get_docs(elem) or a['label'], methods, + [for_field(e) for e in elem.getchildren() if e.tag == 'field']) + +def for_constant(elem): # for <constant> + a = elem.attrib + return Constant(a['name'], int(a['value']), a.get('class', ''), get_docs(elem)) + + +def get_constants(xml): + return [for_constant(e) for e in xml.findall('constant')] + +def get_classes(xml): + return [for_class(e) for e in xml.findall('class')] + +def get_domains(xml): + return [for_domain(e) for e in xml.findall('domain')] + + +def as_unicode(callable): + def roll(*args, **kwargs): + return six.text_type(callable(*args, **kwargs)) + return roll + +def to_dict_by_name(list_of_things): + return dict((a.name, a) for a in list_of_things) + +@as_unicode +def name_class(classname): + """Change AMQP class name to Python class name""" + return classname.capitalize() + +@as_unicode +def format_method_class_name(methodname): + if '-' in methodname: + i = methodname.find('-') + return methodname[0:i].capitalize() + methodname[i+1].upper() + methodname[i+2:] + else: + return methodname.capitalize() + +@as_unicode +def format_field_name(field): + if field in (u'global', u'type'): + field = field + '_' + return field.replace('-', '_') + +def frepr(p, sop=six.text_type): + if isinstance(p, basestring): + p = sop(p) + s = repr(p) + + if isinstance(p, basestring) and not s.startswith('u'): + return ('u' if sop == six.text_type else 'b') + s + else: + return s + +def to_code_binary(p): + body = [] + for q in p: + z = (hex(ord(q))[2:].upper()) + if len(z) == 1: + z = u'0' + z + body.append(u'\\x' + z) + return u"b'"+(u''.join(body))+u"'" + +def pythonify_name(p): + return p.strip().replace('-', '_').upper() + +def try_to_int(p): + try: + return int(p) + except ValueError: + return p + +def to_docstring(label, doc, prefix=4, blank=True): # output a full docstring section + label = [] if label is None else [label] + doc = [] if doc is None else [q.strip() for q in doc.split(u'\n') if len(q.strip()) > 0] + pre = u' '*prefix + + doc = label + doc + + if len(doc) == 0: + return u'' + + doc[0] = doc[0].capitalize() + + if len(doc) == 1: + return doc[0] + + doc = filter(lambda p: len(p.strip()) > 0, doc) + + if blank: + doc = [doc[0], u''] + doc[1:] + + f = (u'\n'.join(pre + lin for lin in doc))[prefix:] + return f + +def ffmt(data, *args, **kwargs): + for arg in args: + op = str if kwargs.get('sane', True) else frepr + data = data.replace('%s', op(arg), 1) + return data diff --git a/coolamqp/framing/definitions.py b/coolamqp/framing/definitions.py new file mode 100644 index 0000000000000000000000000000000000000000..83251c5b6541bb2b74b99ee088a19f4f696e7c08 --- /dev/null +++ b/coolamqp/framing/definitions.py @@ -0,0 +1,4063 @@ +# coding=UTF-8 +from __future__ import print_function, absolute_import +""" +A Python version of the AMQP machine-readable specification. + +Generated automatically by CoolAMQP from AMQP machine-readable specification. +See coolamqp.uplink.framing.compilation for the tool + +AMQP is copyright (c) 2016 OASIS +CoolAMQP is copyright (c) 2016 DMS Serwis s.c. +""" + +import struct, collections, warnings, logging, six + +from coolamqp.framing.base import AMQPClass, AMQPMethodPayload, AMQPContentPropertyList +from coolamqp.framing.field_table import enframe_table, deframe_table, frame_table_size +from coolamqp.framing.compilation.content_property import compile_particular_content_property_list_class + +logger = logging.getLogger(__name__) + +Field = collections.namedtuple('Field', ('name', 'type', 'basic_type', 'reserved')) + +# Core constants +FRAME_METHOD = 1 +FRAME_HEADER = 2 +FRAME_BODY = 3 +FRAME_HEARTBEAT = 8 +FRAME_MIN_SIZE = 4096 +FRAME_END = 206 +REPLY_SUCCESS = 200 # Indicates that the method completed successfully. This reply code is + # reserved for future use - the current protocol design does not use positive + # confirmation and reply codes are sent only in case of an error. +CONTENT_TOO_LARGE = 311 # The client attempted to transfer content larger than the server could accept + # at the present time. The client may retry at a later time. +NO_CONSUMERS = 313 # When the exchange cannot deliver to a consumer when the immediate flag is + # set. As a result of pending data on the queue or the absence of any + # consumers of the queue. +CONNECTION_FORCED = 320 # An operator intervened to close the connection for some reason. The client + # may retry at some later date. +INVALID_PATH = 402 # The client tried to work with an unknown virtual host. +ACCESS_REFUSED = 403 # The client attempted to work with a server entity to which it has no + # access due to security settings. +NOT_FOUND = 404 # The client attempted to work with a server entity that does not exist. +RESOURCE_LOCKED = 405 # The client attempted to work with a server entity to which it has no + # access because another client is working with it. +PRECONDITION_FAILED = 406 # The client requested a method that was not allowed because some precondition + # failed. +FRAME_ERROR = 501 # The sender sent a malformed frame that the recipient could not decode. + # This strongly implies a programming error in the sending peer. +SYNTAX_ERROR = 502 # The sender sent a frame that contained illegal values for one or more + # fields. This strongly implies a programming error in the sending peer. +COMMAND_INVALID = 503 # The client sent an invalid sequence of frames, attempting to perform an + # operation that was considered invalid by the server. This usually implies + # a programming error in the client. +CHANNEL_ERROR = 504 # The client attempted to work with a channel that had not been correctly + # opened. This most likely indicates a fault in the client layer. +UNEXPECTED_FRAME = 505 # The peer sent a frame that was not expected, usually in the context of + # a content header and body. This strongly indicates a fault in the peer's + # content processing. +RESOURCE_ERROR = 506 # The server could not complete the method because it lacked sufficient + # resources. This may be due to the client creating too many of some type + # of entity. +NOT_ALLOWED = 530 # The client tried to work with some entity in a manner that is prohibited + # by the server, due to security settings or by some other criteria. +NOT_IMPLEMENTED = 540 # The client tried to use functionality that is not implemented in the + # server. +INTERNAL_ERROR = 541 # The server could not complete the method because of an internal error. + # The server may require intervention by an operator in order to resume + # normal operations. + +HARD_ERROR = [CONNECTION_FORCED, INVALID_PATH, FRAME_ERROR, SYNTAX_ERROR, COMMAND_INVALID, CHANNEL_ERROR, UNEXPECTED_FRAME, RESOURCE_ERROR, NOT_ALLOWED, NOT_IMPLEMENTED, INTERNAL_ERROR] +SOFT_ERROR = [CONTENT_TOO_LARGE, NO_CONSUMERS, ACCESS_REFUSED, NOT_FOUND, RESOURCE_LOCKED, PRECONDITION_FAILED] + + +DOMAIN_TO_BASIC_TYPE = { + u'class-id': u'short', + u'consumer-tag': u'shortstr', + u'delivery-tag': u'longlong', + u'exchange-name': u'shortstr', + u'method-id': u'short', + u'no-ack': u'bit', + u'no-local': u'bit', + u'no-wait': u'bit', + u'path': u'shortstr', + u'peer-properties': u'table', + u'queue-name': u'shortstr', + u'redelivered': u'bit', + u'message-count': u'long', + u'reply-code': u'short', + u'reply-text': u'shortstr', + u'bit': None, + u'octet': None, + u'short': None, + u'long': None, + u'longlong': None, + u'shortstr': None, + u'longstr': None, + u'timestamp': None, + u'table': None, +} + +class Connection(AMQPClass): + """ + The connection class provides methods for a client to establish a network connection to + + a server, and for both peers to operate the connection thereafter. + """ + NAME = u'connection' + INDEX = 10 + + +class ConnectionBlocked(AMQPMethodPayload): + """ + This method indicates that a connection has been blocked + + and does not accept new publishes. + """ + __slots__ = (u'reason', ) + + NAME = u'connection.blocked' + + INDEX = (10, 60) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x0A\x00\x3C' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = True, True + + IS_SIZE_STATIC = False # this means that argument part has always the same length + IS_CONTENT_STATIC = False # this means that argument part has always the same content + + # See constructor pydoc for details + FIELDS = [ + Field(u'reason', u'shortstr', u'shortstr', reserved=False), + ] + + def __init__(self, reason): + """ + Create frame connection.blocked + + :type reason: binary type (max length 255) (shortstr in AMQP) + """ + self.reason = reason + + def write_arguments(self, buf): + buf.write(struct.pack('!B', len(self.reason))) + buf.write(self.reason) + + def get_size(self): + return 1 + len(self.reason) + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + s_len, = struct.unpack_from('!B', buf, offset) + offset += 1 + reason = buf[offset:offset+s_len] + offset += s_len + return ConnectionBlocked(reason) + + +class ConnectionClose(AMQPMethodPayload): + """ + Request a connection close + + This method indicates that the sender wants to close the connection. This may be + due to internal conditions (e.g. a forced shut-down) or due to an error handling + a specific method, i.e. an exception. When a close is due to an exception, the + sender provides the class and method id of the method which caused the exception. + """ + __slots__ = (u'reply_code', u'reply_text', u'class_id', u'method_id', ) + + NAME = u'connection.close' + + INDEX = (10, 50) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x0A\x00\x32' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = True, True + + IS_SIZE_STATIC = False # this means that argument part has always the same length + IS_CONTENT_STATIC = False # this means that argument part has always the same content + + # See constructor pydoc for details + FIELDS = [ + Field(u'reply-code', u'reply-code', u'short', reserved=False), + Field(u'reply-text', u'reply-text', u'shortstr', reserved=False), + Field(u'class-id', u'class-id', u'short', reserved=False), + Field(u'method-id', u'method-id', u'short', reserved=False), + ] + + def __init__(self, reply_code, reply_text, class_id, method_id): + """ + Create frame connection.close + + :type reply_code: int, 16 bit unsigned (reply-code in AMQP) + :type reply_text: binary type (max length 255) (reply-text in AMQP) + :param class_id: Failing method class + When the close is provoked by a method exception, this is the class of the + method. + :type class_id: int, 16 bit unsigned (class-id in AMQP) + :param method_id: Failing method id + When the close is provoked by a method exception, this is the ID of the method. + :type method_id: int, 16 bit unsigned (method-id in AMQP) + """ + self.reply_code = reply_code + self.reply_text = reply_text + self.class_id = class_id + self.method_id = method_id + + def write_arguments(self, buf): + buf.write(struct.pack('!HB', self.reply_code, len(self.reply_text))) + buf.write(self.reply_text) + buf.write(struct.pack('!HH', self.class_id, self.method_id)) + + def get_size(self): + return 7 + len(self.reply_text) + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + reply_code, s_len, = struct.unpack_from('!HB', buf, offset) + offset += 3 + reply_text = buf[offset:offset+s_len] + offset += s_len + class_id, method_id, = struct.unpack_from('!HH', buf, offset) + offset += 4 + return ConnectionClose(reply_code, reply_text, class_id, method_id) + + +class ConnectionCloseOk(AMQPMethodPayload): + """ + Confirm a connection close + + This method confirms a Connection.Close method and tells the recipient that it is + safe to release resources for the connection and close the socket. + """ + __slots__ = () + + NAME = u'connection.close-ok' + + INDEX = (10, 51) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x0A\x00\x33' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = True, True + + IS_SIZE_STATIC = True # this means that argument part has always the same length + IS_CONTENT_STATIC = True # this means that argument part has always the same content + STATIC_CONTENT = b'\x00\x00\x00\x04\x00\x0A\x00\x33\xCE' # spans LENGTH, CLASS ID, METHOD ID, ....., FRAME_END + + def __init__(self): + """ + Create frame connection.close-ok + """ + + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + return ConnectionCloseOk() + + +class ConnectionOpen(AMQPMethodPayload): + """ + Open connection to virtual host + + This method opens a connection to a virtual host, which is a collection of + resources, and acts to separate multiple application domains within a server. + The server may apply arbitrary limits per virtual host, such as the number + of each type of entity that may be used, per connection and/or in total. + """ + __slots__ = (u'virtual_host', ) + + NAME = u'connection.open' + + INDEX = (10, 40) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x0A\x00\x28' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = True, False + + IS_SIZE_STATIC = False # this means that argument part has always the same length + IS_CONTENT_STATIC = False # this means that argument part has always the same content + + # See constructor pydoc for details + FIELDS = [ + Field(u'virtual-host', u'path', u'shortstr', reserved=False), + Field(u'reserved-1', u'shortstr', u'shortstr', reserved=True), + Field(u'reserved-2', u'bit', u'bit', reserved=True), + ] + + def __init__(self, virtual_host): + """ + Create frame connection.open + + :param virtual_host: Virtual host name + The name of the virtual host to work with. + :type virtual_host: binary type (max length 255) (path in AMQP) + """ + self.virtual_host = virtual_host + + def write_arguments(self, buf): + buf.write(struct.pack('!B', len(self.virtual_host))) + buf.write(self.virtual_host) + buf.write(b'\x00') + buf.write(struct.pack('!B', 0)) + + def get_size(self): + return 3 + len(self.virtual_host) + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + s_len, = struct.unpack_from('!B', buf, offset) + offset += 1 + virtual_host = buf[offset:offset+s_len] + offset += s_len + s_len, = struct.unpack_from('!B', buf, offset) + offset += 1 + offset += s_len # reserved field! + offset += 1 + return ConnectionOpen(virtual_host) + + +class ConnectionOpenOk(AMQPMethodPayload): + """ + Signal that connection is ready + + This method signals to the client that the connection is ready for use. + """ + __slots__ = () + + NAME = u'connection.open-ok' + + INDEX = (10, 41) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x0A\x00\x29' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = False, True + + IS_SIZE_STATIC = False # this means that argument part has always the same length + IS_CONTENT_STATIC = True # this means that argument part has always the same content + STATIC_CONTENT = b'\x00\x00\x00\x04\x00\x0A\x00\x29\x00\xCE' # spans LENGTH, CLASS ID, METHOD ID, ....., FRAME_END + + # See constructor pydoc for details + FIELDS = [ + Field(u'reserved-1', u'shortstr', u'shortstr', reserved=True), + ] + + def __init__(self): + """ + Create frame connection.open-ok + """ + + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + s_len, = struct.unpack_from('!B', buf, offset) + offset += 1 + offset += s_len # reserved field! + return ConnectionOpenOk() + + +class ConnectionStart(AMQPMethodPayload): + """ + Start connection negotiation + + This method starts the connection negotiation process by telling the client the + protocol version that the server proposes, along with a list of security mechanisms + which the client can use for authentication. + """ + __slots__ = (u'version_major', u'version_minor', u'server_properties', u'mechanisms', u'locales', ) + + NAME = u'connection.start' + + INDEX = (10, 10) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x0A\x00\x0A' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = False, True + + IS_SIZE_STATIC = False # this means that argument part has always the same length + IS_CONTENT_STATIC = False # this means that argument part has always the same content + + # See constructor pydoc for details + FIELDS = [ + Field(u'version-major', u'octet', u'octet', reserved=False), + Field(u'version-minor', u'octet', u'octet', reserved=False), + Field(u'server-properties', u'peer-properties', u'table', reserved=False), + Field(u'mechanisms', u'longstr', u'longstr', reserved=False), + Field(u'locales', u'longstr', u'longstr', reserved=False), + ] + + def __init__(self, version_major, version_minor, server_properties, mechanisms, locales): + """ + Create frame connection.start + + :param version_major: Protocol major version + The major version number can take any value from 0 to 99 as defined in the + AMQP specification. + :type version_major: int, 8 bit unsigned (octet in AMQP) + :param version_minor: Protocol minor version + The minor version number can take any value from 0 to 99 as defined in the + AMQP specification. + :type version_minor: int, 8 bit unsigned (octet in AMQP) + :param server_properties: Server properties + The properties SHOULD contain at least these fields: "host", specifying the + server host name or address, "product", giving the name of the server product, + "version", giving the name of the server version, "platform", giving the name + of the operating system, "copyright", if appropriate, and "information", giving + other general information. + :type server_properties: table. See coolamqp.uplink.framing.field_table (peer-properties in AMQP) + :param mechanisms: Available security mechanisms + A list of the security mechanisms that the server supports, delimited by spaces. + :type mechanisms: binary type (longstr in AMQP) + :param locales: Available message locales + A list of the message locales that the server supports, delimited by spaces. The + locale defines the language in which the server will send reply texts. + :type locales: binary type (longstr in AMQP) + """ + self.version_major = version_major + self.version_minor = version_minor + self.server_properties = server_properties + self.mechanisms = mechanisms + self.locales = locales + + def write_arguments(self, buf): + buf.write(struct.pack('!BB', self.version_major, self.version_minor)) + enframe_table(buf, self.server_properties) + buf.write(struct.pack('!I', len(self.mechanisms))) + buf.write(self.mechanisms) + buf.write(struct.pack('!I', len(self.locales))) + buf.write(self.locales) + + def get_size(self): + return 10 + frame_table_size(self.server_properties) + len(self.mechanisms) + len(self.locales) + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + version_major, version_minor, = struct.unpack_from('!BB', buf, offset) + offset += 2 + server_properties, delta = deframe_table(buf, offset) + offset += delta + s_len, = struct.unpack_from('!L', buf, offset) + offset += 4 + mechanisms = buf[offset:offset+s_len] + offset += s_len + s_len, = struct.unpack_from('!L', buf, offset) + offset += 4 + locales = buf[offset:offset+s_len] + offset += s_len + return ConnectionStart(version_major, version_minor, server_properties, mechanisms, locales) + + +class ConnectionSecure(AMQPMethodPayload): + """ + Security mechanism challenge + + The SASL protocol works by exchanging challenges and responses until both peers have + received sufficient information to authenticate each other. This method challenges + the client to provide more information. + """ + __slots__ = (u'challenge', ) + + NAME = u'connection.secure' + + INDEX = (10, 20) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x0A\x00\x14' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = False, True + + IS_SIZE_STATIC = False # this means that argument part has always the same length + IS_CONTENT_STATIC = False # this means that argument part has always the same content + + # See constructor pydoc for details + FIELDS = [ + Field(u'challenge', u'longstr', u'longstr', reserved=False), + ] + + def __init__(self, challenge): + """ + Create frame connection.secure + + :param challenge: Security challenge data + Challenge information, a block of opaque binary data passed to the security + mechanism. + :type challenge: binary type (longstr in AMQP) + """ + self.challenge = challenge + + def write_arguments(self, buf): + buf.write(struct.pack('!I', len(self.challenge))) + buf.write(self.challenge) + + def get_size(self): + return 4 + len(self.challenge) + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + s_len, = struct.unpack_from('!L', buf, offset) + offset += 4 + challenge = buf[offset:offset+s_len] + offset += s_len + return ConnectionSecure(challenge) + + +class ConnectionStartOk(AMQPMethodPayload): + """ + Select security mechanism and locale + + This method selects a SASL security mechanism. + """ + __slots__ = (u'client_properties', u'mechanism', u'response', u'locale', ) + + NAME = u'connection.start-ok' + + INDEX = (10, 11) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x0A\x00\x0B' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = True, False + + IS_SIZE_STATIC = False # this means that argument part has always the same length + IS_CONTENT_STATIC = False # this means that argument part has always the same content + + # See constructor pydoc for details + FIELDS = [ + Field(u'client-properties', u'peer-properties', u'table', reserved=False), + Field(u'mechanism', u'shortstr', u'shortstr', reserved=False), + Field(u'response', u'longstr', u'longstr', reserved=False), + Field(u'locale', u'shortstr', u'shortstr', reserved=False), + ] + + def __init__(self, client_properties, mechanism, response, locale): + """ + Create frame connection.start-ok + + :param client_properties: Client properties + The properties SHOULD contain at least these fields: "product", giving the name + of the client product, "version", giving the name of the client version, "platform", + giving the name of the operating system, "copyright", if appropriate, and + "information", giving other general information. + :type client_properties: table. See coolamqp.uplink.framing.field_table (peer-properties in AMQP) + :param mechanism: Selected security mechanism + A single security mechanisms selected by the client, which must be one of those + specified by the server. + :type mechanism: binary type (max length 255) (shortstr in AMQP) + :param response: Security response data + A block of opaque data passed to the security mechanism. The contents of this + data are defined by the SASL security mechanism. + :type response: binary type (longstr in AMQP) + :param locale: Selected message locale + A single message locale selected by the client, which must be one of those + specified by the server. + :type locale: binary type (max length 255) (shortstr in AMQP) + """ + self.client_properties = client_properties + self.mechanism = mechanism + self.response = response + self.locale = locale + + def write_arguments(self, buf): + enframe_table(buf, self.client_properties) + buf.write(struct.pack('!B', len(self.mechanism))) + buf.write(self.mechanism) + buf.write(struct.pack('!I', len(self.response))) + buf.write(self.response) + buf.write(struct.pack('!B', len(self.locale))) + buf.write(self.locale) + + def get_size(self): + return 6 + frame_table_size(self.client_properties) + len(self.mechanism) + len(self.response) + len(self.locale) + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + client_properties, delta = deframe_table(buf, offset) + offset += delta + s_len, = struct.unpack_from('!B', buf, offset) + offset += 1 + mechanism = buf[offset:offset+s_len] + offset += s_len + s_len, = struct.unpack_from('!L', buf, offset) + offset += 4 + response = buf[offset:offset+s_len] + offset += s_len + s_len, = struct.unpack_from('!B', buf, offset) + offset += 1 + locale = buf[offset:offset+s_len] + offset += s_len + return ConnectionStartOk(client_properties, mechanism, response, locale) + + +class ConnectionSecureOk(AMQPMethodPayload): + """ + Security mechanism response + + This method attempts to authenticate, passing a block of SASL data for the security + mechanism at the server side. + """ + __slots__ = (u'response', ) + + NAME = u'connection.secure-ok' + + INDEX = (10, 21) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x0A\x00\x15' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = True, False + + IS_SIZE_STATIC = False # this means that argument part has always the same length + IS_CONTENT_STATIC = False # this means that argument part has always the same content + + # See constructor pydoc for details + FIELDS = [ + Field(u'response', u'longstr', u'longstr', reserved=False), + ] + + def __init__(self, response): + """ + Create frame connection.secure-ok + + :param response: Security response data + A block of opaque data passed to the security mechanism. The contents of this + data are defined by the SASL security mechanism. + :type response: binary type (longstr in AMQP) + """ + self.response = response + + def write_arguments(self, buf): + buf.write(struct.pack('!I', len(self.response))) + buf.write(self.response) + + def get_size(self): + return 4 + len(self.response) + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + s_len, = struct.unpack_from('!L', buf, offset) + offset += 4 + response = buf[offset:offset+s_len] + offset += s_len + return ConnectionSecureOk(response) + + +class ConnectionTune(AMQPMethodPayload): + """ + Propose connection tuning parameters + + This method proposes a set of connection configuration values to the client. The + client can accept and/or adjust these. + """ + __slots__ = (u'channel_max', u'frame_max', u'heartbeat', ) + + NAME = u'connection.tune' + + INDEX = (10, 30) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x0A\x00\x1E' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = False, True + + IS_SIZE_STATIC = True # this means that argument part has always the same length + IS_CONTENT_STATIC = False # this means that argument part has always the same content + + # See constructor pydoc for details + FIELDS = [ + Field(u'channel-max', u'short', u'short', reserved=False), + Field(u'frame-max', u'long', u'long', reserved=False), + Field(u'heartbeat', u'short', u'short', reserved=False), + ] + + def __init__(self, channel_max, frame_max, heartbeat): + """ + Create frame connection.tune + + :param channel_max: Proposed maximum channels + Specifies highest channel number that the server permits. Usable channel numbers + are in the range 1..channel-max. Zero indicates no specified limit. + :type channel_max: int, 16 bit unsigned (short in AMQP) + :param frame_max: Proposed maximum frame size + The largest frame size that the server proposes for the connection, including + frame header and end-byte. The client can negotiate a lower value. Zero means + that the server does not impose any specific limit but may reject very large + frames if it cannot allocate resources for them. + :type frame_max: int, 32 bit unsigned (long in AMQP) + :param heartbeat: Desired heartbeat delay + The delay, in seconds, of the connection heartbeat that the server wants. + Zero means the server does not want a heartbeat. + :type heartbeat: int, 16 bit unsigned (short in AMQP) + """ + self.channel_max = channel_max + self.frame_max = frame_max + self.heartbeat = heartbeat + + def write_arguments(self, buf): + buf.write(struct.pack('!HIH', self.channel_max, self.frame_max, self.heartbeat)) + + def get_size(self): + return 8 + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + channel_max, frame_max, heartbeat, = struct.unpack_from('!HIH', buf, offset) + offset += 8 + return ConnectionTune(channel_max, frame_max, heartbeat) + + +class ConnectionTuneOk(AMQPMethodPayload): + """ + Negotiate connection tuning parameters + + This method sends the client's connection tuning parameters to the server. + Certain fields are negotiated, others provide capability information. + """ + __slots__ = (u'channel_max', u'frame_max', u'heartbeat', ) + + NAME = u'connection.tune-ok' + + INDEX = (10, 31) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x0A\x00\x1F' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = True, False + + IS_SIZE_STATIC = True # this means that argument part has always the same length + IS_CONTENT_STATIC = False # this means that argument part has always the same content + + # See constructor pydoc for details + FIELDS = [ + Field(u'channel-max', u'short', u'short', reserved=False), + Field(u'frame-max', u'long', u'long', reserved=False), + Field(u'heartbeat', u'short', u'short', reserved=False), + ] + + def __init__(self, channel_max, frame_max, heartbeat): + """ + Create frame connection.tune-ok + + :param channel_max: Negotiated maximum channels + The maximum total number of channels that the client will use per connection. + :type channel_max: int, 16 bit unsigned (short in AMQP) + :param frame_max: Negotiated maximum frame size + The largest frame size that the client and server will use for the connection. + Zero means that the client does not impose any specific limit but may reject + very large frames if it cannot allocate resources for them. Note that the + frame-max limit applies principally to content frames, where large contents can + be broken into frames of arbitrary size. + :type frame_max: int, 32 bit unsigned (long in AMQP) + :param heartbeat: Desired heartbeat delay + The delay, in seconds, of the connection heartbeat that the client wants. Zero + means the client does not want a heartbeat. + :type heartbeat: int, 16 bit unsigned (short in AMQP) + """ + self.channel_max = channel_max + self.frame_max = frame_max + self.heartbeat = heartbeat + + def write_arguments(self, buf): + buf.write(struct.pack('!HIH', self.channel_max, self.frame_max, self.heartbeat)) + + def get_size(self): + return 8 + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + channel_max, frame_max, heartbeat, = struct.unpack_from('!HIH', buf, offset) + offset += 8 + return ConnectionTuneOk(channel_max, frame_max, heartbeat) + + +class ConnectionUnblocked(AMQPMethodPayload): + """ + This method indicates that a connection has been unblocked + + and now accepts publishes. + """ + __slots__ = () + + NAME = u'connection.unblocked' + + INDEX = (10, 61) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x0A\x00\x3D' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = True, True + + IS_SIZE_STATIC = True # this means that argument part has always the same length + IS_CONTENT_STATIC = True # this means that argument part has always the same content + STATIC_CONTENT = b'\x00\x00\x00\x04\x00\x0A\x00\x3D\xCE' # spans LENGTH, CLASS ID, METHOD ID, ....., FRAME_END + + def __init__(self): + """ + Create frame connection.unblocked + """ + + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + return ConnectionUnblocked() + + +class Channel(AMQPClass): + """ + The channel class provides methods for a client to establish a channel to a + + server and for both peers to operate the channel thereafter. + """ + NAME = u'channel' + INDEX = 20 + + +class ChannelClose(AMQPMethodPayload): + """ + Request a channel close + + This method indicates that the sender wants to close the channel. This may be due to + internal conditions (e.g. a forced shut-down) or due to an error handling a specific + method, i.e. an exception. When a close is due to an exception, the sender provides + the class and method id of the method which caused the exception. + """ + __slots__ = (u'reply_code', u'reply_text', u'class_id', u'method_id', ) + + NAME = u'channel.close' + + INDEX = (20, 40) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x14\x00\x28' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = True, True + + IS_SIZE_STATIC = False # this means that argument part has always the same length + IS_CONTENT_STATIC = False # this means that argument part has always the same content + + # See constructor pydoc for details + FIELDS = [ + Field(u'reply-code', u'reply-code', u'short', reserved=False), + Field(u'reply-text', u'reply-text', u'shortstr', reserved=False), + Field(u'class-id', u'class-id', u'short', reserved=False), + Field(u'method-id', u'method-id', u'short', reserved=False), + ] + + def __init__(self, reply_code, reply_text, class_id, method_id): + """ + Create frame channel.close + + :type reply_code: int, 16 bit unsigned (reply-code in AMQP) + :type reply_text: binary type (max length 255) (reply-text in AMQP) + :param class_id: Failing method class + When the close is provoked by a method exception, this is the class of the + method. + :type class_id: int, 16 bit unsigned (class-id in AMQP) + :param method_id: Failing method id + When the close is provoked by a method exception, this is the ID of the method. + :type method_id: int, 16 bit unsigned (method-id in AMQP) + """ + self.reply_code = reply_code + self.reply_text = reply_text + self.class_id = class_id + self.method_id = method_id + + def write_arguments(self, buf): + buf.write(struct.pack('!HB', self.reply_code, len(self.reply_text))) + buf.write(self.reply_text) + buf.write(struct.pack('!HH', self.class_id, self.method_id)) + + def get_size(self): + return 7 + len(self.reply_text) + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + reply_code, s_len, = struct.unpack_from('!HB', buf, offset) + offset += 3 + reply_text = buf[offset:offset+s_len] + offset += s_len + class_id, method_id, = struct.unpack_from('!HH', buf, offset) + offset += 4 + return ChannelClose(reply_code, reply_text, class_id, method_id) + + +class ChannelCloseOk(AMQPMethodPayload): + """ + Confirm a channel close + + This method confirms a Channel.Close method and tells the recipient that it is safe + to release resources for the channel. + """ + __slots__ = () + + NAME = u'channel.close-ok' + + INDEX = (20, 41) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x14\x00\x29' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = True, True + + IS_SIZE_STATIC = True # this means that argument part has always the same length + IS_CONTENT_STATIC = True # this means that argument part has always the same content + STATIC_CONTENT = b'\x00\x00\x00\x04\x00\x14\x00\x29\xCE' # spans LENGTH, CLASS ID, METHOD ID, ....., FRAME_END + + def __init__(self): + """ + Create frame channel.close-ok + """ + + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + return ChannelCloseOk() + + +class ChannelFlow(AMQPMethodPayload): + """ + Enable/disable flow from peer + + This method asks the peer to pause or restart the flow of content data sent by + a consumer. This is a simple flow-control mechanism that a peer can use to avoid + overflowing its queues or otherwise finding itself receiving more messages than + it can process. Note that this method is not intended for window control. It does + not affect contents returned by Basic.Get-Ok methods. + """ + __slots__ = (u'active', ) + + NAME = u'channel.flow' + + INDEX = (20, 20) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x14\x00\x14' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = True, True + + IS_SIZE_STATIC = True # this means that argument part has always the same length + IS_CONTENT_STATIC = False # this means that argument part has always the same content + + # See constructor pydoc for details + FIELDS = [ + Field(u'active', u'bit', u'bit', reserved=False), + ] + + def __init__(self, active): + """ + Create frame channel.flow + + :param active: Start/stop content frames + If 1, the peer starts sending content frames. If 0, the peer stops sending + content frames. + :type active: bool (bit in AMQP) + """ + self.active = active + + def write_arguments(self, buf): + buf.write(struct.pack('!B', (self.active << 0))) + + def get_size(self): + return 1 + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + _bit, = struct.unpack_from('!B', buf, offset) + offset += 0 + active = bool(_bit >> 0) + offset += 1 + return ChannelFlow(active) + + +class ChannelFlowOk(AMQPMethodPayload): + """ + Confirm a flow method + + Confirms to the peer that a flow command was received and processed. + """ + __slots__ = (u'active', ) + + NAME = u'channel.flow-ok' + + INDEX = (20, 21) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x14\x00\x15' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = True, True + + IS_SIZE_STATIC = True # this means that argument part has always the same length + IS_CONTENT_STATIC = False # this means that argument part has always the same content + + # See constructor pydoc for details + FIELDS = [ + Field(u'active', u'bit', u'bit', reserved=False), + ] + + def __init__(self, active): + """ + Create frame channel.flow-ok + + :param active: Current flow setting + Confirms the setting of the processed flow method: 1 means the peer will start + sending or continue to send content frames; 0 means it will not. + :type active: bool (bit in AMQP) + """ + self.active = active + + def write_arguments(self, buf): + buf.write(struct.pack('!B', (self.active << 0))) + + def get_size(self): + return 1 + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + _bit, = struct.unpack_from('!B', buf, offset) + offset += 0 + active = bool(_bit >> 0) + offset += 1 + return ChannelFlowOk(active) + + +class ChannelOpen(AMQPMethodPayload): + """ + Open a channel for use + + This method opens a channel to the server. + """ + __slots__ = () + + NAME = u'channel.open' + + INDEX = (20, 10) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x14\x00\x0A' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = True, False + + IS_SIZE_STATIC = False # this means that argument part has always the same length + IS_CONTENT_STATIC = True # this means that argument part has always the same content + STATIC_CONTENT = b'\x00\x00\x00\x05\x00\x14\x00\x0A\x00\xCE' # spans LENGTH, CLASS ID, METHOD ID, ....., FRAME_END + + # See constructor pydoc for details + FIELDS = [ + Field(u'reserved-1', u'shortstr', u'shortstr', reserved=True), + ] + + def __init__(self): + """ + Create frame channel.open + """ + + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + s_len, = struct.unpack_from('!B', buf, offset) + offset += 1 + offset += s_len # reserved field! + return ChannelOpen() + + +class ChannelOpenOk(AMQPMethodPayload): + """ + Signal that the channel is ready + + This method signals to the client that the channel is ready for use. + """ + __slots__ = () + + NAME = u'channel.open-ok' + + INDEX = (20, 11) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x14\x00\x0B' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = False, True + + IS_SIZE_STATIC = False # this means that argument part has always the same length + IS_CONTENT_STATIC = True # this means that argument part has always the same content + STATIC_CONTENT = b'\x00\x00\x00\x05\x00\x14\x00\x0B\x00\x00\x00\x00\xCE' # spans LENGTH, CLASS ID, METHOD ID, ....., FRAME_END + + # See constructor pydoc for details + FIELDS = [ + Field(u'reserved-1', u'longstr', u'longstr', reserved=True), + ] + + def __init__(self): + """ + Create frame channel.open-ok + """ + + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + s_len, = struct.unpack_from('!L', buf, offset) + offset += 4 + offset += s_len # reserved field! + return ChannelOpenOk() + + +class Exchange(AMQPClass): + """ + Exchanges match and distribute messages across queues. exchanges can be configured in + + the server or declared at runtime. + """ + NAME = u'exchange' + INDEX = 40 + + +class ExchangeBind(AMQPMethodPayload): + """ + Bind exchange to an exchange + + This method binds an exchange to an exchange. + """ + __slots__ = (u'destination', u'source', u'routing_key', u'no_wait', u'arguments', ) + + NAME = u'exchange.bind' + + INDEX = (40, 30) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x28\x00\x1E' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = True, False + + IS_SIZE_STATIC = False # this means that argument part has always the same length + IS_CONTENT_STATIC = False # this means that argument part has always the same content + + # See constructor pydoc for details + FIELDS = [ + Field(u'reserved-1', u'short', u'short', reserved=True), + Field(u'destination', u'exchange-name', u'shortstr', reserved=False), + Field(u'source', u'exchange-name', u'shortstr', reserved=False), + Field(u'routing-key', u'shortstr', u'shortstr', reserved=False), + Field(u'no-wait', u'no-wait', u'bit', reserved=False), + Field(u'arguments', u'table', u'table', reserved=False), + ] + + def __init__(self, destination, source, routing_key, no_wait, arguments): + """ + Create frame exchange.bind + + :param destination: Name of the destination exchange to bind to + Specifies the name of the destination exchange to bind. + :type destination: binary type (max length 255) (exchange-name in AMQP) + :param source: Name of the source exchange to bind to + Specifies the name of the source exchange to bind. + :type source: binary type (max length 255) (exchange-name in AMQP) + :param routing_key: Message routing key + Specifies the routing key for the binding. The routing key + is used for routing messages depending on the exchange + configuration. Not all exchanges use a routing key - refer + to the specific exchange documentation. + :type routing_key: binary type (max length 255) (shortstr in AMQP) + :type no_wait: bool (no-wait in AMQP) + :param arguments: Arguments for binding + A set of arguments for the binding. The syntax and semantics + of these arguments depends on the exchange class. + :type arguments: table. See coolamqp.uplink.framing.field_table (table in AMQP) + """ + self.destination = destination + self.source = source + self.routing_key = routing_key + self.no_wait = no_wait + self.arguments = arguments + + def write_arguments(self, buf): + buf.write(b'\x00\x00') + buf.write(struct.pack('!B', len(self.destination))) + buf.write(self.destination) + buf.write(struct.pack('!B', len(self.source))) + buf.write(self.source) + buf.write(struct.pack('!B', len(self.routing_key))) + buf.write(self.routing_key) + buf.write(struct.pack('!B', (self.no_wait << 0))) + enframe_table(buf, self.arguments) + + def get_size(self): + return 6 + len(self.destination) + len(self.source) + len(self.routing_key) + frame_table_size(self.arguments) + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + s_len, = struct.unpack_from('!2xB', buf, offset) + offset += 3 + destination = buf[offset:offset+s_len] + offset += s_len + s_len, = struct.unpack_from('!B', buf, offset) + offset += 1 + source = buf[offset:offset+s_len] + offset += s_len + s_len, = struct.unpack_from('!B', buf, offset) + offset += 1 + routing_key = buf[offset:offset+s_len] + offset += s_len + _bit, = struct.unpack_from('!B', buf, offset) + offset += 0 + no_wait = bool(_bit >> 0) + offset += 1 + arguments, delta = deframe_table(buf, offset) + offset += delta + return ExchangeBind(destination, source, routing_key, no_wait, arguments) + + +class ExchangeBindOk(AMQPMethodPayload): + """ + Confirm bind successful + + This method confirms that the bind was successful. + """ + __slots__ = () + + NAME = u'exchange.bind-ok' + + INDEX = (40, 31) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x28\x00\x1F' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = False, True + + IS_SIZE_STATIC = True # this means that argument part has always the same length + IS_CONTENT_STATIC = True # this means that argument part has always the same content + STATIC_CONTENT = b'\x00\x00\x00\x04\x00\x28\x00\x1F\xCE' # spans LENGTH, CLASS ID, METHOD ID, ....., FRAME_END + + def __init__(self): + """ + Create frame exchange.bind-ok + """ + + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + return ExchangeBindOk() + + +class ExchangeDeclare(AMQPMethodPayload): + """ + Verify exchange exists, create if needed + + This method creates an exchange if it does not already exist, and if the exchange + exists, verifies that it is of the correct and expected class. + """ + __slots__ = (u'exchange', u'type_', u'passive', u'durable', u'auto_delete', u'internal', u'no_wait', u'arguments', ) + + NAME = u'exchange.declare' + + INDEX = (40, 10) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x28\x00\x0A' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = True, False + + IS_SIZE_STATIC = False # this means that argument part has always the same length + IS_CONTENT_STATIC = False # this means that argument part has always the same content + + # See constructor pydoc for details + FIELDS = [ + Field(u'reserved-1', u'short', u'short', reserved=True), + Field(u'exchange', u'exchange-name', u'shortstr', reserved=False), + Field(u'type', u'shortstr', u'shortstr', reserved=False), + Field(u'passive', u'bit', u'bit', reserved=False), + Field(u'durable', u'bit', u'bit', reserved=False), + Field(u'auto-delete', u'bit', u'bit', reserved=False), + Field(u'internal', u'bit', u'bit', reserved=False), + Field(u'no-wait', u'no-wait', u'bit', reserved=False), + Field(u'arguments', u'table', u'table', reserved=False), + ] + + def __init__(self, exchange, type_, passive, durable, auto_delete, internal, no_wait, arguments): + """ + Create frame exchange.declare + + :param exchange: Exchange names starting with "amq." are reserved for pre-declared and + standardised exchanges. The client MAY declare an exchange starting with + "amq." if the passive option is set, or the exchange already exists. + :type exchange: binary type (max length 255) (exchange-name in AMQP) + :param type_: Exchange type + Each exchange belongs to one of a set of exchange types implemented by the + server. The exchange types define the functionality of the exchange - i.e. how + messages are routed through it. It is not valid or meaningful to attempt to + change the type of an existing exchange. + :type type_: binary type (max length 255) (shortstr in AMQP) + :param passive: Do not create exchange + If set, the server will reply with Declare-Ok if the exchange already + exists with the same name, and raise an error if not. The client can + use this to check whether an exchange exists without modifying the + server state. When set, all other method fields except name and no-wait + are ignored. A declare with both passive and no-wait has no effect. + Arguments are compared for semantic equivalence. + :type passive: bool (bit in AMQP) + :param durable: Request a durable exchange + If set when creating a new exchange, the exchange will be marked as durable. + Durable exchanges remain active when a server restarts. Non-durable exchanges + (transient exchanges) are purged if/when a server restarts. + :type durable: bool (bit in AMQP) + :param auto_delete: Auto-delete when unused + If set, the exchange is deleted when all queues have + finished using it. + :type auto_delete: bool (bit in AMQP) + :param internal: Create internal exchange + If set, the exchange may not be used directly by publishers, + but only when bound to other exchanges. Internal exchanges + are used to construct wiring that is not visible to + applications. + :type internal: bool (bit in AMQP) + :type no_wait: bool (no-wait in AMQP) + :param arguments: Arguments for declaration + A set of arguments for the declaration. The syntax and semantics of these + arguments depends on the server implementation. + :type arguments: table. See coolamqp.uplink.framing.field_table (table in AMQP) + """ + self.exchange = exchange + self.type_ = type_ + self.passive = passive + self.durable = durable + self.auto_delete = auto_delete + self.internal = internal + self.no_wait = no_wait + self.arguments = arguments + + def write_arguments(self, buf): + buf.write(b'\x00\x00') + buf.write(struct.pack('!B', len(self.exchange))) + buf.write(self.exchange) + buf.write(struct.pack('!B', len(self.type_))) + buf.write(self.type_) + buf.write(struct.pack('!B', (self.passive << 0) | (self.durable << 1) | (self.auto_delete << 2) | (self.internal << 3) | (self.no_wait << 4))) + enframe_table(buf, self.arguments) + + def get_size(self): + return 5 + len(self.exchange) + len(self.type_) + frame_table_size(self.arguments) + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + s_len, = struct.unpack_from('!2xB', buf, offset) + offset += 3 + exchange = buf[offset:offset+s_len] + offset += s_len + s_len, = struct.unpack_from('!B', buf, offset) + offset += 1 + type_ = buf[offset:offset+s_len] + offset += s_len + _bit, = struct.unpack_from('!B', buf, offset) + offset += 0 + passive = bool(_bit >> 0) + durable = bool(_bit >> 1) + auto_delete = bool(_bit >> 2) + internal = bool(_bit >> 3) + no_wait = bool(_bit >> 4) + offset += 1 + arguments, delta = deframe_table(buf, offset) + offset += delta + return ExchangeDeclare(exchange, type_, passive, durable, auto_delete, internal, no_wait, arguments) + + +class ExchangeDelete(AMQPMethodPayload): + """ + Delete an exchange + + This method deletes an exchange. When an exchange is deleted all queue bindings on + the exchange are cancelled. + """ + __slots__ = (u'exchange', u'if_unused', u'no_wait', ) + + NAME = u'exchange.delete' + + INDEX = (40, 20) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x28\x00\x14' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = True, False + + IS_SIZE_STATIC = False # this means that argument part has always the same length + IS_CONTENT_STATIC = False # this means that argument part has always the same content + + # See constructor pydoc for details + FIELDS = [ + Field(u'reserved-1', u'short', u'short', reserved=True), + Field(u'exchange', u'exchange-name', u'shortstr', reserved=False), + Field(u'if-unused', u'bit', u'bit', reserved=False), + Field(u'no-wait', u'no-wait', u'bit', reserved=False), + ] + + def __init__(self, exchange, if_unused, no_wait): + """ + Create frame exchange.delete + + :param exchange: The client must not attempt to delete an exchange that does not exist. + :type exchange: binary type (max length 255) (exchange-name in AMQP) + :param if_unused: Delete only if unused + If set, the server will only delete the exchange if it has no queue bindings. If + the exchange has queue bindings the server does not delete it but raises a + channel exception instead. + :type if_unused: bool (bit in AMQP) + :type no_wait: bool (no-wait in AMQP) + """ + self.exchange = exchange + self.if_unused = if_unused + self.no_wait = no_wait + + def write_arguments(self, buf): + buf.write(b'\x00\x00') + buf.write(struct.pack('!B', len(self.exchange))) + buf.write(self.exchange) + buf.write(struct.pack('!B', (self.if_unused << 0) | (self.no_wait << 1))) + + def get_size(self): + return 4 + len(self.exchange) + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + s_len, = struct.unpack_from('!2xB', buf, offset) + offset += 3 + exchange = buf[offset:offset+s_len] + offset += s_len + _bit, = struct.unpack_from('!B', buf, offset) + offset += 0 + if_unused = bool(_bit >> 0) + no_wait = bool(_bit >> 1) + offset += 1 + return ExchangeDelete(exchange, if_unused, no_wait) + + +class ExchangeDeclareOk(AMQPMethodPayload): + """ + Confirm exchange declaration + + This method confirms a Declare method and confirms the name of the exchange, + essential for automatically-named exchanges. + """ + __slots__ = () + + NAME = u'exchange.declare-ok' + + INDEX = (40, 11) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x28\x00\x0B' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = False, True + + IS_SIZE_STATIC = True # this means that argument part has always the same length + IS_CONTENT_STATIC = True # this means that argument part has always the same content + STATIC_CONTENT = b'\x00\x00\x00\x04\x00\x28\x00\x0B\xCE' # spans LENGTH, CLASS ID, METHOD ID, ....., FRAME_END + + def __init__(self): + """ + Create frame exchange.declare-ok + """ + + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + return ExchangeDeclareOk() + + +class ExchangeDeleteOk(AMQPMethodPayload): + """ + Confirm deletion of an exchange + + This method confirms the deletion of an exchange. + """ + __slots__ = () + + NAME = u'exchange.delete-ok' + + INDEX = (40, 21) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x28\x00\x15' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = False, True + + IS_SIZE_STATIC = True # this means that argument part has always the same length + IS_CONTENT_STATIC = True # this means that argument part has always the same content + STATIC_CONTENT = b'\x00\x00\x00\x04\x00\x28\x00\x15\xCE' # spans LENGTH, CLASS ID, METHOD ID, ....., FRAME_END + + def __init__(self): + """ + Create frame exchange.delete-ok + """ + + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + return ExchangeDeleteOk() + + +class ExchangeUnbind(AMQPMethodPayload): + """ + Unbind an exchange from an exchange + + This method unbinds an exchange from an exchange. + """ + __slots__ = (u'destination', u'source', u'routing_key', u'no_wait', u'arguments', ) + + NAME = u'exchange.unbind' + + INDEX = (40, 40) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x28\x00\x28' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = True, False + + IS_SIZE_STATIC = False # this means that argument part has always the same length + IS_CONTENT_STATIC = False # this means that argument part has always the same content + + # See constructor pydoc for details + FIELDS = [ + Field(u'reserved-1', u'short', u'short', reserved=True), + Field(u'destination', u'exchange-name', u'shortstr', reserved=False), + Field(u'source', u'exchange-name', u'shortstr', reserved=False), + Field(u'routing-key', u'shortstr', u'shortstr', reserved=False), + Field(u'no-wait', u'no-wait', u'bit', reserved=False), + Field(u'arguments', u'table', u'table', reserved=False), + ] + + def __init__(self, destination, source, routing_key, no_wait, arguments): + """ + Create frame exchange.unbind + + :param destination: Specifies the name of the destination exchange to unbind. + :type destination: binary type (max length 255) (exchange-name in AMQP) + :param source: Specifies the name of the source exchange to unbind. + :type source: binary type (max length 255) (exchange-name in AMQP) + :param routing_key: Routing key of binding + Specifies the routing key of the binding to unbind. + :type routing_key: binary type (max length 255) (shortstr in AMQP) + :type no_wait: bool (no-wait in AMQP) + :param arguments: Arguments of binding + Specifies the arguments of the binding to unbind. + :type arguments: table. See coolamqp.uplink.framing.field_table (table in AMQP) + """ + self.destination = destination + self.source = source + self.routing_key = routing_key + self.no_wait = no_wait + self.arguments = arguments + + def write_arguments(self, buf): + buf.write(b'\x00\x00') + buf.write(struct.pack('!B', len(self.destination))) + buf.write(self.destination) + buf.write(struct.pack('!B', len(self.source))) + buf.write(self.source) + buf.write(struct.pack('!B', len(self.routing_key))) + buf.write(self.routing_key) + buf.write(struct.pack('!B', (self.no_wait << 0))) + enframe_table(buf, self.arguments) + + def get_size(self): + return 6 + len(self.destination) + len(self.source) + len(self.routing_key) + frame_table_size(self.arguments) + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + s_len, = struct.unpack_from('!2xB', buf, offset) + offset += 3 + destination = buf[offset:offset+s_len] + offset += s_len + s_len, = struct.unpack_from('!B', buf, offset) + offset += 1 + source = buf[offset:offset+s_len] + offset += s_len + s_len, = struct.unpack_from('!B', buf, offset) + offset += 1 + routing_key = buf[offset:offset+s_len] + offset += s_len + _bit, = struct.unpack_from('!B', buf, offset) + offset += 0 + no_wait = bool(_bit >> 0) + offset += 1 + arguments, delta = deframe_table(buf, offset) + offset += delta + return ExchangeUnbind(destination, source, routing_key, no_wait, arguments) + + +class ExchangeUnbindOk(AMQPMethodPayload): + """ + Confirm unbind successful + + This method confirms that the unbind was successful. + """ + __slots__ = () + + NAME = u'exchange.unbind-ok' + + INDEX = (40, 51) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x28\x00\x33' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = False, True + + IS_SIZE_STATIC = True # this means that argument part has always the same length + IS_CONTENT_STATIC = True # this means that argument part has always the same content + STATIC_CONTENT = b'\x00\x00\x00\x04\x00\x28\x00\x33\xCE' # spans LENGTH, CLASS ID, METHOD ID, ....., FRAME_END + + def __init__(self): + """ + Create frame exchange.unbind-ok + """ + + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + return ExchangeUnbindOk() + + +class Queue(AMQPClass): + """ + Queues store and forward messages. queues can be configured in the server or created at + + runtime. Queues must be attached to at least one exchange in order to receive messages + from publishers. + """ + NAME = u'queue' + INDEX = 50 + + +class QueueBind(AMQPMethodPayload): + """ + Bind queue to an exchange + + This method binds a queue to an exchange. Until a queue is bound it will not + receive any messages. In a classic messaging model, store-and-forward queues + are bound to a direct exchange and subscription queues are bound to a topic + exchange. + """ + __slots__ = (u'queue', u'exchange', u'routing_key', u'no_wait', u'arguments', ) + + NAME = u'queue.bind' + + INDEX = (50, 20) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x32\x00\x14' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = True, False + + IS_SIZE_STATIC = False # this means that argument part has always the same length + IS_CONTENT_STATIC = False # this means that argument part has always the same content + + # See constructor pydoc for details + FIELDS = [ + Field(u'reserved-1', u'short', u'short', reserved=True), + Field(u'queue', u'queue-name', u'shortstr', reserved=False), + Field(u'exchange', u'exchange-name', u'shortstr', reserved=False), + Field(u'routing-key', u'shortstr', u'shortstr', reserved=False), + Field(u'no-wait', u'no-wait', u'bit', reserved=False), + Field(u'arguments', u'table', u'table', reserved=False), + ] + + def __init__(self, queue, exchange, routing_key, no_wait, arguments): + """ + Create frame queue.bind + + :param queue: Specifies the name of the queue to bind. + :type queue: binary type (max length 255) (queue-name in AMQP) + :param exchange: Name of the exchange to bind to + A client MUST NOT be allowed to bind a queue to a non-existent exchange. + :type exchange: binary type (max length 255) (exchange-name in AMQP) + :param routing_key: Message routing key + Specifies the routing key for the binding. The routing key is used for routing + messages depending on the exchange configuration. Not all exchanges use a + routing key - refer to the specific exchange documentation. If the queue name + is empty, the server uses the last queue declared on the channel. If the + routing key is also empty, the server uses this queue name for the routing + key as well. If the queue name is provided but the routing key is empty, the + server does the binding with that empty routing key. The meaning of empty + routing keys depends on the exchange implementation. + :type routing_key: binary type (max length 255) (shortstr in AMQP) + :type no_wait: bool (no-wait in AMQP) + :param arguments: Arguments for binding + A set of arguments for the binding. The syntax and semantics of these arguments + depends on the exchange class. + :type arguments: table. See coolamqp.uplink.framing.field_table (table in AMQP) + """ + self.queue = queue + self.exchange = exchange + self.routing_key = routing_key + self.no_wait = no_wait + self.arguments = arguments + + def write_arguments(self, buf): + buf.write(b'\x00\x00') + buf.write(struct.pack('!B', len(self.queue))) + buf.write(self.queue) + buf.write(struct.pack('!B', len(self.exchange))) + buf.write(self.exchange) + buf.write(struct.pack('!B', len(self.routing_key))) + buf.write(self.routing_key) + buf.write(struct.pack('!B', (self.no_wait << 0))) + enframe_table(buf, self.arguments) + + def get_size(self): + return 6 + len(self.queue) + len(self.exchange) + len(self.routing_key) + frame_table_size(self.arguments) + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + s_len, = struct.unpack_from('!2xB', buf, offset) + offset += 3 + queue = buf[offset:offset+s_len] + offset += s_len + s_len, = struct.unpack_from('!B', buf, offset) + offset += 1 + exchange = buf[offset:offset+s_len] + offset += s_len + s_len, = struct.unpack_from('!B', buf, offset) + offset += 1 + routing_key = buf[offset:offset+s_len] + offset += s_len + _bit, = struct.unpack_from('!B', buf, offset) + offset += 0 + no_wait = bool(_bit >> 0) + offset += 1 + arguments, delta = deframe_table(buf, offset) + offset += delta + return QueueBind(queue, exchange, routing_key, no_wait, arguments) + + +class QueueBindOk(AMQPMethodPayload): + """ + Confirm bind successful + + This method confirms that the bind was successful. + """ + __slots__ = () + + NAME = u'queue.bind-ok' + + INDEX = (50, 21) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x32\x00\x15' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = False, True + + IS_SIZE_STATIC = True # this means that argument part has always the same length + IS_CONTENT_STATIC = True # this means that argument part has always the same content + STATIC_CONTENT = b'\x00\x00\x00\x04\x00\x32\x00\x15\xCE' # spans LENGTH, CLASS ID, METHOD ID, ....., FRAME_END + + def __init__(self): + """ + Create frame queue.bind-ok + """ + + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + return QueueBindOk() + + +class QueueDeclare(AMQPMethodPayload): + """ + Declare queue, create if needed + + This method creates or checks a queue. When creating a new queue the client can + specify various properties that control the durability of the queue and its + contents, and the level of sharing for the queue. + """ + __slots__ = (u'queue', u'passive', u'durable', u'exclusive', u'auto_delete', u'no_wait', u'arguments', ) + + NAME = u'queue.declare' + + INDEX = (50, 10) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x32\x00\x0A' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = True, False + + IS_SIZE_STATIC = False # this means that argument part has always the same length + IS_CONTENT_STATIC = False # this means that argument part has always the same content + + # See constructor pydoc for details + FIELDS = [ + Field(u'reserved-1', u'short', u'short', reserved=True), + Field(u'queue', u'queue-name', u'shortstr', reserved=False), + Field(u'passive', u'bit', u'bit', reserved=False), + Field(u'durable', u'bit', u'bit', reserved=False), + Field(u'exclusive', u'bit', u'bit', reserved=False), + Field(u'auto-delete', u'bit', u'bit', reserved=False), + Field(u'no-wait', u'no-wait', u'bit', reserved=False), + Field(u'arguments', u'table', u'table', reserved=False), + ] + + def __init__(self, queue, passive, durable, exclusive, auto_delete, no_wait, arguments): + """ + Create frame queue.declare + + :param queue: The queue name may be empty, in which case the server must create a new + queue with a unique generated name and return this to the client in the + Declare-Ok method. + :type queue: binary type (max length 255) (queue-name in AMQP) + :param passive: Do not create queue + If set, the server will reply with Declare-Ok if the queue already + exists with the same name, and raise an error if not. The client can + use this to check whether a queue exists without modifying the + server state. When set, all other method fields except name and no-wait + are ignored. A declare with both passive and no-wait has no effect. + Arguments are compared for semantic equivalence. + :type passive: bool (bit in AMQP) + :param durable: Request a durable queue + If set when creating a new queue, the queue will be marked as durable. Durable + queues remain active when a server restarts. Non-durable queues (transient + queues) are purged if/when a server restarts. Note that durable queues do not + necessarily hold persistent messages, although it does not make sense to send + persistent messages to a transient queue. + :type durable: bool (bit in AMQP) + :param exclusive: Request an exclusive queue + Exclusive queues may only be accessed by the current connection, and are + deleted when that connection closes. Passive declaration of an exclusive + queue by other connections are not allowed. + :type exclusive: bool (bit in AMQP) + :param auto_delete: Auto-delete queue when unused + If set, the queue is deleted when all consumers have finished using it. The last + consumer can be cancelled either explicitly or because its channel is closed. If + there was no consumer ever on the queue, it won't be deleted. Applications can + explicitly delete auto-delete queues using the Delete method as normal. + :type auto_delete: bool (bit in AMQP) + :type no_wait: bool (no-wait in AMQP) + :param arguments: Arguments for declaration + A set of arguments for the declaration. The syntax and semantics of these + arguments depends on the server implementation. + :type arguments: table. See coolamqp.uplink.framing.field_table (table in AMQP) + """ + self.queue = queue + self.passive = passive + self.durable = durable + self.exclusive = exclusive + self.auto_delete = auto_delete + self.no_wait = no_wait + self.arguments = arguments + + def write_arguments(self, buf): + buf.write(b'\x00\x00') + buf.write(struct.pack('!B', len(self.queue))) + buf.write(self.queue) + buf.write(struct.pack('!B', (self.passive << 0) | (self.durable << 1) | (self.exclusive << 2) | (self.auto_delete << 3) | (self.no_wait << 4))) + enframe_table(buf, self.arguments) + + def get_size(self): + return 4 + len(self.queue) + frame_table_size(self.arguments) + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + s_len, = struct.unpack_from('!2xB', buf, offset) + offset += 3 + queue = buf[offset:offset+s_len] + offset += s_len + _bit, = struct.unpack_from('!B', buf, offset) + offset += 0 + passive = bool(_bit >> 0) + durable = bool(_bit >> 1) + exclusive = bool(_bit >> 2) + auto_delete = bool(_bit >> 3) + no_wait = bool(_bit >> 4) + offset += 1 + arguments, delta = deframe_table(buf, offset) + offset += delta + return QueueDeclare(queue, passive, durable, exclusive, auto_delete, no_wait, arguments) + + +class QueueDelete(AMQPMethodPayload): + """ + Delete a queue + + This method deletes a queue. When a queue is deleted any pending messages are sent + to a dead-letter queue if this is defined in the server configuration, and all + consumers on the queue are cancelled. + """ + __slots__ = (u'queue', u'if_unused', u'if_empty', u'no_wait', ) + + NAME = u'queue.delete' + + INDEX = (50, 40) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x32\x00\x28' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = True, False + + IS_SIZE_STATIC = False # this means that argument part has always the same length + IS_CONTENT_STATIC = False # this means that argument part has always the same content + + # See constructor pydoc for details + FIELDS = [ + Field(u'reserved-1', u'short', u'short', reserved=True), + Field(u'queue', u'queue-name', u'shortstr', reserved=False), + Field(u'if-unused', u'bit', u'bit', reserved=False), + Field(u'if-empty', u'bit', u'bit', reserved=False), + Field(u'no-wait', u'no-wait', u'bit', reserved=False), + ] + + def __init__(self, queue, if_unused, if_empty, no_wait): + """ + Create frame queue.delete + + :param queue: Specifies the name of the queue to delete. + :type queue: binary type (max length 255) (queue-name in AMQP) + :param if_unused: Delete only if unused + If set, the server will only delete the queue if it has no consumers. If the + queue has consumers the server does does not delete it but raises a channel + exception instead. + :type if_unused: bool (bit in AMQP) + :param if_empty: Delete only if empty + If set, the server will only delete the queue if it has no messages. + :type if_empty: bool (bit in AMQP) + :type no_wait: bool (no-wait in AMQP) + """ + self.queue = queue + self.if_unused = if_unused + self.if_empty = if_empty + self.no_wait = no_wait + + def write_arguments(self, buf): + buf.write(b'\x00\x00') + buf.write(struct.pack('!B', len(self.queue))) + buf.write(self.queue) + buf.write(struct.pack('!B', (self.if_unused << 0) | (self.if_empty << 1) | (self.no_wait << 2))) + + def get_size(self): + return 4 + len(self.queue) + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + s_len, = struct.unpack_from('!2xB', buf, offset) + offset += 3 + queue = buf[offset:offset+s_len] + offset += s_len + _bit, = struct.unpack_from('!B', buf, offset) + offset += 0 + if_unused = bool(_bit >> 0) + if_empty = bool(_bit >> 1) + no_wait = bool(_bit >> 2) + offset += 1 + return QueueDelete(queue, if_unused, if_empty, no_wait) + + +class QueueDeclareOk(AMQPMethodPayload): + """ + Confirms a queue definition + + This method confirms a Declare method and confirms the name of the queue, essential + for automatically-named queues. + """ + __slots__ = (u'queue', u'message_count', u'consumer_count', ) + + NAME = u'queue.declare-ok' + + INDEX = (50, 11) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x32\x00\x0B' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = False, True + + IS_SIZE_STATIC = False # this means that argument part has always the same length + IS_CONTENT_STATIC = False # this means that argument part has always the same content + + # See constructor pydoc for details + FIELDS = [ + Field(u'queue', u'queue-name', u'shortstr', reserved=False), + Field(u'message-count', u'message-count', u'long', reserved=False), + Field(u'consumer-count', u'long', u'long', reserved=False), + ] + + def __init__(self, queue, message_count, consumer_count): + """ + Create frame queue.declare-ok + + :param queue: Reports the name of the queue. if the server generated a queue name, this field + contains that name. + :type queue: binary type (max length 255) (queue-name in AMQP) + :type message_count: int, 32 bit unsigned (message-count in AMQP) + :param consumer_count: Number of consumers + Reports the number of active consumers for the queue. Note that consumers can + suspend activity (Channel.Flow) in which case they do not appear in this count. + :type consumer_count: int, 32 bit unsigned (long in AMQP) + """ + self.queue = queue + self.message_count = message_count + self.consumer_count = consumer_count + + def write_arguments(self, buf): + buf.write(struct.pack('!B', len(self.queue))) + buf.write(self.queue) + buf.write(struct.pack('!II', self.message_count, self.consumer_count)) + + def get_size(self): + return 9 + len(self.queue) + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + s_len, = struct.unpack_from('!B', buf, offset) + offset += 1 + queue = buf[offset:offset+s_len] + offset += s_len + message_count, consumer_count, = struct.unpack_from('!II', buf, offset) + offset += 8 + return QueueDeclareOk(queue, message_count, consumer_count) + + +class QueueDeleteOk(AMQPMethodPayload): + """ + Confirm deletion of a queue + + This method confirms the deletion of a queue. + """ + __slots__ = (u'message_count', ) + + NAME = u'queue.delete-ok' + + INDEX = (50, 41) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x32\x00\x29' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = False, True + + IS_SIZE_STATIC = True # this means that argument part has always the same length + IS_CONTENT_STATIC = False # this means that argument part has always the same content + + # See constructor pydoc for details + FIELDS = [ + Field(u'message-count', u'message-count', u'long', reserved=False), + ] + + def __init__(self, message_count): + """ + Create frame queue.delete-ok + + :param message_count: Reports the number of messages deleted. + :type message_count: int, 32 bit unsigned (message-count in AMQP) + """ + self.message_count = message_count + + def write_arguments(self, buf): + buf.write(struct.pack('!I', self.message_count)) + + def get_size(self): + return 4 + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + message_count, = struct.unpack_from('!I', buf, offset) + offset += 4 + return QueueDeleteOk(message_count) + + +class QueuePurge(AMQPMethodPayload): + """ + Purge a queue + + This method removes all messages from a queue which are not awaiting + acknowledgment. + """ + __slots__ = (u'queue', u'no_wait', ) + + NAME = u'queue.purge' + + INDEX = (50, 30) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x32\x00\x1E' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = True, False + + IS_SIZE_STATIC = False # this means that argument part has always the same length + IS_CONTENT_STATIC = False # this means that argument part has always the same content + + # See constructor pydoc for details + FIELDS = [ + Field(u'reserved-1', u'short', u'short', reserved=True), + Field(u'queue', u'queue-name', u'shortstr', reserved=False), + Field(u'no-wait', u'no-wait', u'bit', reserved=False), + ] + + def __init__(self, queue, no_wait): + """ + Create frame queue.purge + + :param queue: Specifies the name of the queue to purge. + :type queue: binary type (max length 255) (queue-name in AMQP) + :type no_wait: bool (no-wait in AMQP) + """ + self.queue = queue + self.no_wait = no_wait + + def write_arguments(self, buf): + buf.write(b'\x00\x00') + buf.write(struct.pack('!B', len(self.queue))) + buf.write(self.queue) + buf.write(struct.pack('!B', (self.no_wait << 0))) + + def get_size(self): + return 4 + len(self.queue) + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + s_len, = struct.unpack_from('!2xB', buf, offset) + offset += 3 + queue = buf[offset:offset+s_len] + offset += s_len + _bit, = struct.unpack_from('!B', buf, offset) + offset += 0 + no_wait = bool(_bit >> 0) + offset += 1 + return QueuePurge(queue, no_wait) + + +class QueuePurgeOk(AMQPMethodPayload): + """ + Confirms a queue purge + + This method confirms the purge of a queue. + """ + __slots__ = (u'message_count', ) + + NAME = u'queue.purge-ok' + + INDEX = (50, 31) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x32\x00\x1F' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = False, True + + IS_SIZE_STATIC = True # this means that argument part has always the same length + IS_CONTENT_STATIC = False # this means that argument part has always the same content + + # See constructor pydoc for details + FIELDS = [ + Field(u'message-count', u'message-count', u'long', reserved=False), + ] + + def __init__(self, message_count): + """ + Create frame queue.purge-ok + + :param message_count: Reports the number of messages purged. + :type message_count: int, 32 bit unsigned (message-count in AMQP) + """ + self.message_count = message_count + + def write_arguments(self, buf): + buf.write(struct.pack('!I', self.message_count)) + + def get_size(self): + return 4 + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + message_count, = struct.unpack_from('!I', buf, offset) + offset += 4 + return QueuePurgeOk(message_count) + + +class QueueUnbind(AMQPMethodPayload): + """ + Unbind a queue from an exchange + + This method unbinds a queue from an exchange. + """ + __slots__ = (u'queue', u'exchange', u'routing_key', u'arguments', ) + + NAME = u'queue.unbind' + + INDEX = (50, 50) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x32\x00\x32' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = True, False + + IS_SIZE_STATIC = False # this means that argument part has always the same length + IS_CONTENT_STATIC = False # this means that argument part has always the same content + + # See constructor pydoc for details + FIELDS = [ + Field(u'reserved-1', u'short', u'short', reserved=True), + Field(u'queue', u'queue-name', u'shortstr', reserved=False), + Field(u'exchange', u'exchange-name', u'shortstr', reserved=False), + Field(u'routing-key', u'shortstr', u'shortstr', reserved=False), + Field(u'arguments', u'table', u'table', reserved=False), + ] + + def __init__(self, queue, exchange, routing_key, arguments): + """ + Create frame queue.unbind + + :param queue: Specifies the name of the queue to unbind. + :type queue: binary type (max length 255) (queue-name in AMQP) + :param exchange: The name of the exchange to unbind from. + :type exchange: binary type (max length 255) (exchange-name in AMQP) + :param routing_key: Routing key of binding + Specifies the routing key of the binding to unbind. + :type routing_key: binary type (max length 255) (shortstr in AMQP) + :param arguments: Arguments of binding + Specifies the arguments of the binding to unbind. + :type arguments: table. See coolamqp.uplink.framing.field_table (table in AMQP) + """ + self.queue = queue + self.exchange = exchange + self.routing_key = routing_key + self.arguments = arguments + + def write_arguments(self, buf): + buf.write(b'\x00\x00') + buf.write(struct.pack('!B', len(self.queue))) + buf.write(self.queue) + buf.write(struct.pack('!B', len(self.exchange))) + buf.write(self.exchange) + buf.write(struct.pack('!B', len(self.routing_key))) + buf.write(self.routing_key) + enframe_table(buf, self.arguments) + + def get_size(self): + return 5 + len(self.queue) + len(self.exchange) + len(self.routing_key) + frame_table_size(self.arguments) + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + s_len, = struct.unpack_from('!2xB', buf, offset) + offset += 3 + queue = buf[offset:offset+s_len] + offset += s_len + s_len, = struct.unpack_from('!B', buf, offset) + offset += 1 + exchange = buf[offset:offset+s_len] + offset += s_len + s_len, = struct.unpack_from('!B', buf, offset) + offset += 1 + routing_key = buf[offset:offset+s_len] + offset += s_len + arguments, delta = deframe_table(buf, offset) + offset += delta + return QueueUnbind(queue, exchange, routing_key, arguments) + + +class QueueUnbindOk(AMQPMethodPayload): + """ + Confirm unbind successful + + This method confirms that the unbind was successful. + """ + __slots__ = () + + NAME = u'queue.unbind-ok' + + INDEX = (50, 51) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x32\x00\x33' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = False, True + + IS_SIZE_STATIC = True # this means that argument part has always the same length + IS_CONTENT_STATIC = True # this means that argument part has always the same content + STATIC_CONTENT = b'\x00\x00\x00\x04\x00\x32\x00\x33\xCE' # spans LENGTH, CLASS ID, METHOD ID, ....., FRAME_END + + def __init__(self): + """ + Create frame queue.unbind-ok + """ + + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + return QueueUnbindOk() + + +class Basic(AMQPClass): + """ + The basic class provides methods that support an industry-standard messaging model. + """ + NAME = u'basic' + INDEX = 60 + + +class BasicContentPropertyList(AMQPContentPropertyList): + """ + The basic class provides methods that support an industry-standard messaging model. + """ + FIELDS = [ + Field(u'content-type', u'shortstr', u'shortstr', False), + Field(u'content-encoding', u'shortstr', u'shortstr', False), + Field(u'headers', u'table', u'table', False), + Field(u'delivery-mode', u'octet', u'octet', False), + Field(u'priority', u'octet', u'octet', False), + Field(u'correlation-id', u'shortstr', u'shortstr', False), + Field(u'reply-to', u'shortstr', u'shortstr', False), + Field(u'expiration', u'shortstr', u'shortstr', False), + Field(u'message-id', u'shortstr', u'shortstr', False), + Field(u'timestamp', u'timestamp', u'timestamp', False), + Field(u'type', u'shortstr', u'shortstr', False), + Field(u'user-id', u'shortstr', u'shortstr', False), + Field(u'app-id', u'shortstr', u'shortstr', False), + Field(u'reserved', u'shortstr', u'shortstr', False), + ] + # A dictionary from a zero property list to a class typized with + # some fields + PARTICULAR_CLASSES = {} + + def __new__(self, **kwargs): + """ + Return a property list. + :param content_type: MIME content type + :type content_type: binary type (max length 255) (AMQP as shortstr) + :param content_encoding: MIME content encoding + :type content_encoding: binary type (max length 255) (AMQP as shortstr) + :param headers: message header field table + :type headers: table. See coolamqp.uplink.framing.field_table (AMQP as table) + :param delivery_mode: non-persistent (1) or persistent (2) + :type delivery_mode: int, 8 bit unsigned (AMQP as octet) + :param priority: message priority, 0 to 9 + :type priority: int, 8 bit unsigned (AMQP as octet) + :param correlation_id: application correlation identifier + :type correlation_id: binary type (max length 255) (AMQP as shortstr) + :param reply_to: address to reply to + :type reply_to: binary type (max length 255) (AMQP as shortstr) + :param expiration: message expiration specification + :type expiration: binary type (max length 255) (AMQP as shortstr) + :param message_id: application message identifier + :type message_id: binary type (max length 255) (AMQP as shortstr) + :param timestamp: message timestamp + :type timestamp: 64 bit signed POSIX timestamp (in seconds) (AMQP as timestamp) + :param type_: message type name + :type type_: binary type (max length 255) (AMQP as shortstr) + :param user_id: creating user id + :type user_id: binary type (max length 255) (AMQP as shortstr) + :param app_id: creating application id + :type app_id: binary type (max length 255) (AMQP as shortstr) + :param reserved: reserved, must be empty + :type reserved: binary type (max length 255) (AMQP as shortstr) + """ + zpf = bytearray([ + (('content_type' in kwargs) << 7) | (('content_encoding' in kwargs) << 6) | (('headers' in kwargs) << 5) | (('delivery_mode' in kwargs) << 4) | (('priority' in kwargs) << 3) | (('correlation_id' in kwargs) << 2) | (('reply_to' in kwargs) << 1) | int('expiration' in kwargs), + (('message_id' in kwargs) << 7) | (('timestamp' in kwargs) << 6) | (('type_' in kwargs) << 5) | (('user_id' in kwargs) << 4) | (('app_id' in kwargs) << 3) | (('reserved' in kwargs) << 2) + ]) + zpf = six.binary_type(zpf) + + if zpf in BasicContentPropertyList.PARTICULAR_CLASSES: + warnings.warn(u"""You could go faster. + + If you know in advance what properties you will be using, use typized constructors like + + # runs once + my_type = BasicContentPropertyList.typize('content_type', 'content_encoding') + # runs many times + props = my_type('text/plain', 'utf8') + + instead of + + # runs many times + props = BasicContentPropertyList(content_type='text/plain', content_encoding='utf8') + + This way you will be faster. + + If you do not know in advance what properties you will be using, it is correct to use + this constructor. + """) + + return BasicContentPropertyList.PARTICULAR_CLASSES[zpf](**kwargs) + else: + logger.debug('Property field (BasicContentPropertyList:%s) not seen yet, compiling', repr(zpf)) + c = compile_particular_content_property_list_class(zpf, BasicContentPropertyList.FIELDS) + BasicContentPropertyList.PARTICULAR_CLASSES[zpf] = c + return c(**kwargs) + + @staticmethod + def typize(*fields): + zpf = bytearray([ + (('content_type' in fields) << 7) | (('content_encoding' in fields) << 6) | (('headers' in fields) << 5) | (('delivery_mode' in fields) << 4) | (('priority' in fields) << 3) | (('correlation_id' in fields) << 2) | (('reply_to' in fields) << 1) | int('expiration' in kwargs), + (('message_id' in fields) << 7) | (('timestamp' in fields) << 6) | (('type_' in fields) << 5) | (('user_id' in fields) << 4) | (('app_id' in fields) << 3) | (('reserved' in fields) << 2) + ]) + zpf = six.binary_type(zpf) + if zpf in BasicContentPropertyList.PARTICULAR_CLASSES: + return BasicContentPropertyList.PARTICULAR_CLASSES[zpf] + else: + logger.debug('Property field (BasicContentPropertyList:%s) not seen yet, compiling', repr(zpf)) + c = compile_particular_content_property_list_class(zpf, BasicContentPropertyList.FIELDS) + BasicContentPropertyList.PARTICULAR_CLASSES[zpf] = c + return c + + @staticmethod + def from_buffer(buf, offset): + """ + Return a content property list instance unserialized from + buffer, so that buf[offset] marks the start of property flags + """ + # extract property flags + pfl = 2 + while ord(buf[offset + pfl - 1]) & 1: + pfl += 2 + zpf = BasicContentPropertyList.zero_property_flags(buf[offset:offset+pfl]) + if zpf in BasicContentPropertyList.PARTICULAR_CLASSES: + return BasicContentPropertyList.PARTICULAR_CLASSES[zpf].from_buffer(buf, offset) + else: + logger.debug('Property field (BasicContentPropertyList:%s) not seen yet, compiling', repr(zpf)) + c = compile_particular_content_property_list_class(zpf, BasicContentPropertyList.FIELDS) + BasicContentPropertyList.PARTICULAR_CLASSES[zpf] = c + return c.from_buffer(buf, offset) + + +class BasicAck(AMQPMethodPayload): + """ + Acknowledge one or more messages + + When sent by the client, this method acknowledges one or more + messages delivered via the Deliver or Get-Ok methods. + When sent by server, this method acknowledges one or more + messages published with the Publish method on a channel in + confirm mode. + The acknowledgement can be for a single message or a set of + messages up to and including a specific message. + """ + __slots__ = (u'delivery_tag', u'multiple', ) + + NAME = u'basic.ack' + + INDEX = (60, 80) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x3C\x00\x50' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = True, True + + IS_SIZE_STATIC = True # this means that argument part has always the same length + IS_CONTENT_STATIC = False # this means that argument part has always the same content + + # See constructor pydoc for details + FIELDS = [ + Field(u'delivery-tag', u'delivery-tag', u'longlong', reserved=False), + Field(u'multiple', u'bit', u'bit', reserved=False), + ] + + def __init__(self, delivery_tag, multiple): + """ + Create frame basic.ack + + :type delivery_tag: int, 64 bit unsigned (delivery-tag in AMQP) + :param multiple: Acknowledge multiple messages + If set to 1, the delivery tag is treated as "up to and + including", so that multiple messages can be acknowledged + with a single method. If set to zero, the delivery tag + refers to a single message. If the multiple field is 1, and + the delivery tag is zero, this indicates acknowledgement of + all outstanding messages. + :type multiple: bool (bit in AMQP) + """ + self.delivery_tag = delivery_tag + self.multiple = multiple + + def write_arguments(self, buf): + buf.write(struct.pack('!QB', self.delivery_tag, (self.multiple << 0))) + + def get_size(self): + return 9 + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + delivery_tag, _bit, = struct.unpack_from('!QB', buf, offset) + offset += 8 + multiple = bool(_bit >> 0) + offset += 1 + return BasicAck(delivery_tag, multiple) + + +class BasicConsume(AMQPMethodPayload): + """ + Start a queue consumer + + This method asks the server to start a "consumer", which is a transient request for + messages from a specific queue. Consumers last as long as the channel they were + declared on, or until the client cancels them. + """ + __slots__ = (u'queue', u'consumer_tag', u'no_local', u'no_ack', u'exclusive', u'no_wait', u'arguments', ) + + NAME = u'basic.consume' + + INDEX = (60, 20) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x3C\x00\x14' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = True, False + + IS_SIZE_STATIC = False # this means that argument part has always the same length + IS_CONTENT_STATIC = False # this means that argument part has always the same content + + # See constructor pydoc for details + FIELDS = [ + Field(u'reserved-1', u'short', u'short', reserved=True), + Field(u'queue', u'queue-name', u'shortstr', reserved=False), + Field(u'consumer-tag', u'consumer-tag', u'shortstr', reserved=False), + Field(u'no-local', u'no-local', u'bit', reserved=False), + Field(u'no-ack', u'no-ack', u'bit', reserved=False), + Field(u'exclusive', u'bit', u'bit', reserved=False), + Field(u'no-wait', u'no-wait', u'bit', reserved=False), + Field(u'arguments', u'table', u'table', reserved=False), + ] + + def __init__(self, queue, consumer_tag, no_local, no_ack, exclusive, no_wait, arguments): + """ + Create frame basic.consume + + :param queue: Specifies the name of the queue to consume from. + :type queue: binary type (max length 255) (queue-name in AMQP) + :param consumer_tag: Specifies the identifier for the consumer. the consumer tag is local to a + channel, so two clients can use the same consumer tags. If this field is + empty the server will generate a unique tag. + :type consumer_tag: binary type (max length 255) (consumer-tag in AMQP) + :type no_local: bool (no-local in AMQP) + :type no_ack: bool (no-ack in AMQP) + :param exclusive: Request exclusive access + Request exclusive consumer access, meaning only this consumer can access the + queue. + :type exclusive: bool (bit in AMQP) + :type no_wait: bool (no-wait in AMQP) + :param arguments: Arguments for declaration + A set of arguments for the consume. The syntax and semantics of these + arguments depends on the server implementation. + :type arguments: table. See coolamqp.uplink.framing.field_table (table in AMQP) + """ + self.queue = queue + self.consumer_tag = consumer_tag + self.no_local = no_local + self.no_ack = no_ack + self.exclusive = exclusive + self.no_wait = no_wait + self.arguments = arguments + + def write_arguments(self, buf): + buf.write(b'\x00\x00') + buf.write(struct.pack('!B', len(self.queue))) + buf.write(self.queue) + buf.write(struct.pack('!B', len(self.consumer_tag))) + buf.write(self.consumer_tag) + buf.write(struct.pack('!B', (self.no_local << 0) | (self.no_ack << 1) | (self.exclusive << 2) | (self.no_wait << 3))) + enframe_table(buf, self.arguments) + + def get_size(self): + return 5 + len(self.queue) + len(self.consumer_tag) + frame_table_size(self.arguments) + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + s_len, = struct.unpack_from('!2xB', buf, offset) + offset += 3 + queue = buf[offset:offset+s_len] + offset += s_len + s_len, = struct.unpack_from('!B', buf, offset) + offset += 1 + consumer_tag = buf[offset:offset+s_len] + offset += s_len + _bit, = struct.unpack_from('!B', buf, offset) + offset += 0 + no_local = bool(_bit >> 0) + no_ack = bool(_bit >> 1) + exclusive = bool(_bit >> 2) + no_wait = bool(_bit >> 3) + offset += 1 + arguments, delta = deframe_table(buf, offset) + offset += delta + return BasicConsume(queue, consumer_tag, no_local, no_ack, exclusive, no_wait, arguments) + + +class BasicCancel(AMQPMethodPayload): + """ + End a queue consumer + + This method cancels a consumer. This does not affect already delivered + messages, but it does mean the server will not send any more messages for + that consumer. The client may receive an arbitrary number of messages in + between sending the cancel method and receiving the cancel-ok reply. + It may also be sent from the server to the client in the event + of the consumer being unexpectedly cancelled (i.e. cancelled + for any reason other than the server receiving the + corresponding basic.cancel from the client). This allows + clients to be notified of the loss of consumers due to events + such as queue deletion. Note that as it is not a MUST for + clients to accept this method from the server, it is advisable + for the broker to be able to identify those clients that are + capable of accepting the method, through some means of + capability negotiation. + """ + __slots__ = (u'consumer_tag', u'no_wait', ) + + NAME = u'basic.cancel' + + INDEX = (60, 30) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x3C\x00\x1E' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = True, True + + IS_SIZE_STATIC = False # this means that argument part has always the same length + IS_CONTENT_STATIC = False # this means that argument part has always the same content + + # See constructor pydoc for details + FIELDS = [ + Field(u'consumer-tag', u'consumer-tag', u'shortstr', reserved=False), + Field(u'no-wait', u'no-wait', u'bit', reserved=False), + ] + + def __init__(self, consumer_tag, no_wait): + """ + Create frame basic.cancel + + :type consumer_tag: binary type (max length 255) (consumer-tag in AMQP) + :type no_wait: bool (no-wait in AMQP) + """ + self.consumer_tag = consumer_tag + self.no_wait = no_wait + + def write_arguments(self, buf): + buf.write(struct.pack('!B', len(self.consumer_tag))) + buf.write(self.consumer_tag) + buf.write(struct.pack('!B', (self.no_wait << 0))) + + def get_size(self): + return 2 + len(self.consumer_tag) + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + s_len, = struct.unpack_from('!B', buf, offset) + offset += 1 + consumer_tag = buf[offset:offset+s_len] + offset += s_len + _bit, = struct.unpack_from('!B', buf, offset) + offset += 0 + no_wait = bool(_bit >> 0) + offset += 1 + return BasicCancel(consumer_tag, no_wait) + + +class BasicConsumeOk(AMQPMethodPayload): + """ + Confirm a new consumer + + The server provides the client with a consumer tag, which is used by the client + for methods called on the consumer at a later stage. + """ + __slots__ = (u'consumer_tag', ) + + NAME = u'basic.consume-ok' + + INDEX = (60, 21) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x3C\x00\x15' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = False, True + + IS_SIZE_STATIC = False # this means that argument part has always the same length + IS_CONTENT_STATIC = False # this means that argument part has always the same content + + # See constructor pydoc for details + FIELDS = [ + Field(u'consumer-tag', u'consumer-tag', u'shortstr', reserved=False), + ] + + def __init__(self, consumer_tag): + """ + Create frame basic.consume-ok + + :param consumer_tag: Holds the consumer tag specified by the client or provided by the server. + :type consumer_tag: binary type (max length 255) (consumer-tag in AMQP) + """ + self.consumer_tag = consumer_tag + + def write_arguments(self, buf): + buf.write(struct.pack('!B', len(self.consumer_tag))) + buf.write(self.consumer_tag) + + def get_size(self): + return 1 + len(self.consumer_tag) + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + s_len, = struct.unpack_from('!B', buf, offset) + offset += 1 + consumer_tag = buf[offset:offset+s_len] + offset += s_len + return BasicConsumeOk(consumer_tag) + + +class BasicCancelOk(AMQPMethodPayload): + """ + Confirm a cancelled consumer + + This method confirms that the cancellation was completed. + """ + __slots__ = (u'consumer_tag', ) + + NAME = u'basic.cancel-ok' + + INDEX = (60, 31) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x3C\x00\x1F' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = True, True + + IS_SIZE_STATIC = False # this means that argument part has always the same length + IS_CONTENT_STATIC = False # this means that argument part has always the same content + + # See constructor pydoc for details + FIELDS = [ + Field(u'consumer-tag', u'consumer-tag', u'shortstr', reserved=False), + ] + + def __init__(self, consumer_tag): + """ + Create frame basic.cancel-ok + + :type consumer_tag: binary type (max length 255) (consumer-tag in AMQP) + """ + self.consumer_tag = consumer_tag + + def write_arguments(self, buf): + buf.write(struct.pack('!B', len(self.consumer_tag))) + buf.write(self.consumer_tag) + + def get_size(self): + return 1 + len(self.consumer_tag) + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + s_len, = struct.unpack_from('!B', buf, offset) + offset += 1 + consumer_tag = buf[offset:offset+s_len] + offset += s_len + return BasicCancelOk(consumer_tag) + + +class BasicDeliver(AMQPMethodPayload): + """ + Notify the client of a consumer message + + This method delivers a message to the client, via a consumer. In the asynchronous + message delivery model, the client starts a consumer using the Consume method, then + the server responds with Deliver methods as and when messages arrive for that + consumer. + """ + __slots__ = (u'consumer_tag', u'delivery_tag', u'redelivered', u'exchange', u'routing_key', ) + + NAME = u'basic.deliver' + + INDEX = (60, 60) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x3C\x00\x3C' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = False, True + + IS_SIZE_STATIC = False # this means that argument part has always the same length + IS_CONTENT_STATIC = False # this means that argument part has always the same content + + # See constructor pydoc for details + FIELDS = [ + Field(u'consumer-tag', u'consumer-tag', u'shortstr', reserved=False), + Field(u'delivery-tag', u'delivery-tag', u'longlong', reserved=False), + Field(u'redelivered', u'redelivered', u'bit', reserved=False), + Field(u'exchange', u'exchange-name', u'shortstr', reserved=False), + Field(u'routing-key', u'shortstr', u'shortstr', reserved=False), + ] + + def __init__(self, consumer_tag, delivery_tag, redelivered, exchange, routing_key): + """ + Create frame basic.deliver + + :type consumer_tag: binary type (max length 255) (consumer-tag in AMQP) + :type delivery_tag: int, 64 bit unsigned (delivery-tag in AMQP) + :type redelivered: bool (redelivered in AMQP) + :param exchange: Specifies the name of the exchange that the message was originally published to. + May be empty, indicating the default exchange. + :type exchange: binary type (max length 255) (exchange-name in AMQP) + :param routing_key: Message routing key + Specifies the routing key name specified when the message was published. + :type routing_key: binary type (max length 255) (shortstr in AMQP) + """ + self.consumer_tag = consumer_tag + self.delivery_tag = delivery_tag + self.redelivered = redelivered + self.exchange = exchange + self.routing_key = routing_key + + def write_arguments(self, buf): + buf.write(struct.pack('!B', len(self.consumer_tag))) + buf.write(self.consumer_tag) + buf.write(struct.pack('!QBB', self.delivery_tag, (self.redelivered << 0), len(self.exchange))) + buf.write(self.exchange) + buf.write(struct.pack('!B', len(self.routing_key))) + buf.write(self.routing_key) + + def get_size(self): + return 12 + len(self.consumer_tag) + len(self.exchange) + len(self.routing_key) + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + s_len, = struct.unpack_from('!B', buf, offset) + offset += 1 + consumer_tag = buf[offset:offset+s_len] + offset += s_len + delivery_tag, _bit, = struct.unpack_from('!QB', buf, offset) + offset += 8 + redelivered = bool(_bit >> 0) + offset += 1 + s_len, = struct.unpack_from('!B', buf, offset) + offset += 1 + exchange = buf[offset:offset+s_len] + offset += s_len + s_len, = struct.unpack_from('!B', buf, offset) + offset += 1 + routing_key = buf[offset:offset+s_len] + offset += s_len + return BasicDeliver(consumer_tag, delivery_tag, redelivered, exchange, routing_key) + + +class BasicGet(AMQPMethodPayload): + """ + Direct access to a queue + + This method provides a direct access to the messages in a queue using a synchronous + dialogue that is designed for specific types of application where synchronous + functionality is more important than performance. + """ + __slots__ = (u'queue', u'no_ack', ) + + NAME = u'basic.get' + + INDEX = (60, 70) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x3C\x00\x46' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = True, False + + IS_SIZE_STATIC = False # this means that argument part has always the same length + IS_CONTENT_STATIC = False # this means that argument part has always the same content + + # See constructor pydoc for details + FIELDS = [ + Field(u'reserved-1', u'short', u'short', reserved=True), + Field(u'queue', u'queue-name', u'shortstr', reserved=False), + Field(u'no-ack', u'no-ack', u'bit', reserved=False), + ] + + def __init__(self, queue, no_ack): + """ + Create frame basic.get + + :param queue: Specifies the name of the queue to get a message from. + :type queue: binary type (max length 255) (queue-name in AMQP) + :type no_ack: bool (no-ack in AMQP) + """ + self.queue = queue + self.no_ack = no_ack + + def write_arguments(self, buf): + buf.write(b'\x00\x00') + buf.write(struct.pack('!B', len(self.queue))) + buf.write(self.queue) + buf.write(struct.pack('!B', (self.no_ack << 0))) + + def get_size(self): + return 4 + len(self.queue) + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + s_len, = struct.unpack_from('!2xB', buf, offset) + offset += 3 + queue = buf[offset:offset+s_len] + offset += s_len + _bit, = struct.unpack_from('!B', buf, offset) + offset += 0 + no_ack = bool(_bit >> 0) + offset += 1 + return BasicGet(queue, no_ack) + + +class BasicGetOk(AMQPMethodPayload): + """ + Provide client with a message + + This method delivers a message to the client following a get method. A message + delivered by 'get-ok' must be acknowledged unless the no-ack option was set in the + get method. + """ + __slots__ = (u'delivery_tag', u'redelivered', u'exchange', u'routing_key', u'message_count', ) + + NAME = u'basic.get-ok' + + INDEX = (60, 71) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x3C\x00\x47' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = False, True + + IS_SIZE_STATIC = False # this means that argument part has always the same length + IS_CONTENT_STATIC = False # this means that argument part has always the same content + + # See constructor pydoc for details + FIELDS = [ + Field(u'delivery-tag', u'delivery-tag', u'longlong', reserved=False), + Field(u'redelivered', u'redelivered', u'bit', reserved=False), + Field(u'exchange', u'exchange-name', u'shortstr', reserved=False), + Field(u'routing-key', u'shortstr', u'shortstr', reserved=False), + Field(u'message-count', u'message-count', u'long', reserved=False), + ] + + def __init__(self, delivery_tag, redelivered, exchange, routing_key, message_count): + """ + Create frame basic.get-ok + + :type delivery_tag: int, 64 bit unsigned (delivery-tag in AMQP) + :type redelivered: bool (redelivered in AMQP) + :param exchange: Specifies the name of the exchange that the message was originally published to. + If empty, the message was published to the default exchange. + :type exchange: binary type (max length 255) (exchange-name in AMQP) + :param routing_key: Message routing key + Specifies the routing key name specified when the message was published. + :type routing_key: binary type (max length 255) (shortstr in AMQP) + :type message_count: int, 32 bit unsigned (message-count in AMQP) + """ + self.delivery_tag = delivery_tag + self.redelivered = redelivered + self.exchange = exchange + self.routing_key = routing_key + self.message_count = message_count + + def write_arguments(self, buf): + buf.write(struct.pack('!QBB', self.delivery_tag, (self.redelivered << 0), len(self.exchange))) + buf.write(self.exchange) + buf.write(struct.pack('!B', len(self.routing_key))) + buf.write(self.routing_key) + buf.write(struct.pack('!I', self.message_count)) + + def get_size(self): + return 15 + len(self.exchange) + len(self.routing_key) + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + delivery_tag, _bit, = struct.unpack_from('!QB', buf, offset) + offset += 8 + redelivered = bool(_bit >> 0) + offset += 1 + s_len, = struct.unpack_from('!B', buf, offset) + offset += 1 + exchange = buf[offset:offset+s_len] + offset += s_len + s_len, = struct.unpack_from('!B', buf, offset) + offset += 1 + routing_key = buf[offset:offset+s_len] + offset += s_len + message_count, = struct.unpack_from('!I', buf, offset) + offset += 4 + return BasicGetOk(delivery_tag, redelivered, exchange, routing_key, message_count) + + +class BasicGetEmpty(AMQPMethodPayload): + """ + Indicate no messages available + + This method tells the client that the queue has no messages available for the + client. + """ + __slots__ = () + + NAME = u'basic.get-empty' + + INDEX = (60, 72) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x3C\x00\x48' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = False, True + + IS_SIZE_STATIC = False # this means that argument part has always the same length + IS_CONTENT_STATIC = True # this means that argument part has always the same content + STATIC_CONTENT = b'\x00\x00\x00\x0D\x00\x3C\x00\x48\x00\xCE' # spans LENGTH, CLASS ID, METHOD ID, ....., FRAME_END + + # See constructor pydoc for details + FIELDS = [ + Field(u'reserved-1', u'shortstr', u'shortstr', reserved=True), + ] + + def __init__(self): + """ + Create frame basic.get-empty + """ + + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + s_len, = struct.unpack_from('!B', buf, offset) + offset += 1 + offset += s_len # reserved field! + return BasicGetEmpty() + + +class BasicNack(AMQPMethodPayload): + """ + Reject one or more incoming messages + + This method allows a client to reject one or more incoming messages. It can be + used to interrupt and cancel large incoming messages, or return untreatable + messages to their original queue. + This method is also used by the server to inform publishers on channels in + confirm mode of unhandled messages. If a publisher receives this method, it + probably needs to republish the offending messages. + """ + __slots__ = (u'delivery_tag', u'multiple', u'requeue', ) + + NAME = u'basic.nack' + + INDEX = (60, 120) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x3C\x00\x78' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = True, True + + IS_SIZE_STATIC = True # this means that argument part has always the same length + IS_CONTENT_STATIC = False # this means that argument part has always the same content + + # See constructor pydoc for details + FIELDS = [ + Field(u'delivery-tag', u'delivery-tag', u'longlong', reserved=False), + Field(u'multiple', u'bit', u'bit', reserved=False), + Field(u'requeue', u'bit', u'bit', reserved=False), + ] + + def __init__(self, delivery_tag, multiple, requeue): + """ + Create frame basic.nack + + :type delivery_tag: int, 64 bit unsigned (delivery-tag in AMQP) + :param multiple: Reject multiple messages + If set to 1, the delivery tag is treated as "up to and + including", so that multiple messages can be rejected + with a single method. If set to zero, the delivery tag + refers to a single message. If the multiple field is 1, and + the delivery tag is zero, this indicates rejection of + all outstanding messages. + :type multiple: bool (bit in AMQP) + :param requeue: Requeue the message + If requeue is true, the server will attempt to requeue the message. If requeue + is false or the requeue attempt fails the messages are discarded or dead-lettered. + Clients receiving the Nack methods should ignore this flag. + :type requeue: bool (bit in AMQP) + """ + self.delivery_tag = delivery_tag + self.multiple = multiple + self.requeue = requeue + + def write_arguments(self, buf): + buf.write(struct.pack('!QB', self.delivery_tag, (self.multiple << 0) | (self.requeue << 1))) + + def get_size(self): + return 9 + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + delivery_tag, _bit, = struct.unpack_from('!QB', buf, offset) + offset += 8 + multiple = bool(_bit >> 0) + requeue = bool(_bit >> 1) + offset += 1 + return BasicNack(delivery_tag, multiple, requeue) + + +class BasicPublish(AMQPMethodPayload): + """ + Publish a message + + This method publishes a message to a specific exchange. The message will be routed + to queues as defined by the exchange configuration and distributed to any active + consumers when the transaction, if any, is committed. + """ + __slots__ = (u'exchange', u'routing_key', u'mandatory', u'immediate', ) + + NAME = u'basic.publish' + + INDEX = (60, 40) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x3C\x00\x28' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = True, False + + IS_SIZE_STATIC = False # this means that argument part has always the same length + IS_CONTENT_STATIC = False # this means that argument part has always the same content + + # See constructor pydoc for details + FIELDS = [ + Field(u'reserved-1', u'short', u'short', reserved=True), + Field(u'exchange', u'exchange-name', u'shortstr', reserved=False), + Field(u'routing-key', u'shortstr', u'shortstr', reserved=False), + Field(u'mandatory', u'bit', u'bit', reserved=False), + Field(u'immediate', u'bit', u'bit', reserved=False), + ] + + def __init__(self, exchange, routing_key, mandatory, immediate): + """ + Create frame basic.publish + + :param exchange: Specifies the name of the exchange to publish to. the exchange name can be + empty, meaning the default exchange. If the exchange name is specified, and that + exchange does not exist, the server will raise a channel exception. + :type exchange: binary type (max length 255) (exchange-name in AMQP) + :param routing_key: Message routing key + Specifies the routing key for the message. The routing key is used for routing + messages depending on the exchange configuration. + :type routing_key: binary type (max length 255) (shortstr in AMQP) + :param mandatory: Indicate mandatory routing + This flag tells the server how to react if the message cannot be routed to a + queue. If this flag is set, the server will return an unroutable message with a + Return method. If this flag is zero, the server silently drops the message. + :type mandatory: bool (bit in AMQP) + :param immediate: Request immediate delivery + This flag tells the server how to react if the message cannot be routed to a + queue consumer immediately. If this flag is set, the server will return an + undeliverable message with a Return method. If this flag is zero, the server + will queue the message, but with no guarantee that it will ever be consumed. + :type immediate: bool (bit in AMQP) + """ + self.exchange = exchange + self.routing_key = routing_key + self.mandatory = mandatory + self.immediate = immediate + + def write_arguments(self, buf): + buf.write(b'\x00\x00') + buf.write(struct.pack('!B', len(self.exchange))) + buf.write(self.exchange) + buf.write(struct.pack('!B', len(self.routing_key))) + buf.write(self.routing_key) + buf.write(struct.pack('!B', (self.mandatory << 0) | (self.immediate << 1))) + + def get_size(self): + return 5 + len(self.exchange) + len(self.routing_key) + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + s_len, = struct.unpack_from('!2xB', buf, offset) + offset += 3 + exchange = buf[offset:offset+s_len] + offset += s_len + s_len, = struct.unpack_from('!B', buf, offset) + offset += 1 + routing_key = buf[offset:offset+s_len] + offset += s_len + _bit, = struct.unpack_from('!B', buf, offset) + offset += 0 + mandatory = bool(_bit >> 0) + immediate = bool(_bit >> 1) + offset += 1 + return BasicPublish(exchange, routing_key, mandatory, immediate) + + +class BasicQos(AMQPMethodPayload): + """ + Specify quality of service + + This method requests a specific quality of service. The QoS can be specified for the + current channel or for all channels on the connection. The particular properties and + semantics of a qos method always depend on the content class semantics. Though the + qos method could in principle apply to both peers, it is currently meaningful only + for the server. + """ + __slots__ = (u'prefetch_size', u'prefetch_count', u'global_', ) + + NAME = u'basic.qos' + + INDEX = (60, 10) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x3C\x00\x0A' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = True, False + + IS_SIZE_STATIC = True # this means that argument part has always the same length + IS_CONTENT_STATIC = False # this means that argument part has always the same content + + # See constructor pydoc for details + FIELDS = [ + Field(u'prefetch-size', u'long', u'long', reserved=False), + Field(u'prefetch-count', u'short', u'short', reserved=False), + Field(u'global', u'bit', u'bit', reserved=False), + ] + + def __init__(self, prefetch_size, prefetch_count, global_): + """ + Create frame basic.qos + + :param prefetch_size: Prefetch window in octets + The client can request that messages be sent in advance so that when the client + finishes processing a message, the following message is already held locally, + rather than needing to be sent down the channel. Prefetching gives a performance + improvement. This field specifies the prefetch window size in octets. The server + will send a message in advance if it is equal to or smaller in size than the + available prefetch size (and also falls into other prefetch limits). May be set + to zero, meaning "no specific limit", although other prefetch limits may still + apply. The prefetch-size is ignored if the no-ack option is set. + :type prefetch_size: int, 32 bit unsigned (long in AMQP) + :param prefetch_count: Prefetch window in messages + Specifies a prefetch window in terms of whole messages. This field may be used + in combination with the prefetch-size field; a message will only be sent in + advance if both prefetch windows (and those at the channel and connection level) + allow it. The prefetch-count is ignored if the no-ack option is set. + :type prefetch_count: int, 16 bit unsigned (short in AMQP) + :param global_: Apply to entire connection + RabbitMQ has reinterpreted this field. The original + specification said: "By default the QoS settings apply to + the current channel only. If this field is set, they are + applied to the entire connection." Instead, RabbitMQ takes + global=false to mean that the QoS settings should apply + per-consumer (for new consumers on the channel; existing + ones being unaffected) and global=true to mean that the QoS + settings should apply per-channel. + :type global_: bool (bit in AMQP) + """ + self.prefetch_size = prefetch_size + self.prefetch_count = prefetch_count + self.global_ = global_ + + def write_arguments(self, buf): + buf.write(struct.pack('!IHB', self.prefetch_size, self.prefetch_count, (self.global_ << 0))) + + def get_size(self): + return 7 + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + prefetch_size, prefetch_count, _bit, = struct.unpack_from('!IHB', buf, offset) + offset += 6 + global_ = bool(_bit >> 0) + offset += 1 + return BasicQos(prefetch_size, prefetch_count, global_) + + +class BasicQosOk(AMQPMethodPayload): + """ + Confirm the requested qos + + This method tells the client that the requested QoS levels could be handled by the + server. The requested QoS applies to all active consumers until a new QoS is + defined. + """ + __slots__ = () + + NAME = u'basic.qos-ok' + + INDEX = (60, 11) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x3C\x00\x0B' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = False, True + + IS_SIZE_STATIC = True # this means that argument part has always the same length + IS_CONTENT_STATIC = True # this means that argument part has always the same content + STATIC_CONTENT = b'\x00\x00\x00\x04\x00\x3C\x00\x0B\xCE' # spans LENGTH, CLASS ID, METHOD ID, ....., FRAME_END + + def __init__(self): + """ + Create frame basic.qos-ok + """ + + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + return BasicQosOk() + + +class BasicReturn(AMQPMethodPayload): + """ + Return a failed message + + This method returns an undeliverable message that was published with the "immediate" + flag set, or an unroutable message published with the "mandatory" flag set. The + reply code and text provide information about the reason that the message was + undeliverable. + """ + __slots__ = (u'reply_code', u'reply_text', u'exchange', u'routing_key', ) + + NAME = u'basic.return' + + INDEX = (60, 50) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x3C\x00\x32' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = False, True + + IS_SIZE_STATIC = False # this means that argument part has always the same length + IS_CONTENT_STATIC = False # this means that argument part has always the same content + + # See constructor pydoc for details + FIELDS = [ + Field(u'reply-code', u'reply-code', u'short', reserved=False), + Field(u'reply-text', u'reply-text', u'shortstr', reserved=False), + Field(u'exchange', u'exchange-name', u'shortstr', reserved=False), + Field(u'routing-key', u'shortstr', u'shortstr', reserved=False), + ] + + def __init__(self, reply_code, reply_text, exchange, routing_key): + """ + Create frame basic.return + + :type reply_code: int, 16 bit unsigned (reply-code in AMQP) + :type reply_text: binary type (max length 255) (reply-text in AMQP) + :param exchange: Specifies the name of the exchange that the message was originally published + to. May be empty, meaning the default exchange. + :type exchange: binary type (max length 255) (exchange-name in AMQP) + :param routing_key: Message routing key + Specifies the routing key name specified when the message was published. + :type routing_key: binary type (max length 255) (shortstr in AMQP) + """ + self.reply_code = reply_code + self.reply_text = reply_text + self.exchange = exchange + self.routing_key = routing_key + + def write_arguments(self, buf): + buf.write(struct.pack('!HB', self.reply_code, len(self.reply_text))) + buf.write(self.reply_text) + buf.write(struct.pack('!B', len(self.exchange))) + buf.write(self.exchange) + buf.write(struct.pack('!B', len(self.routing_key))) + buf.write(self.routing_key) + + def get_size(self): + return 5 + len(self.reply_text) + len(self.exchange) + len(self.routing_key) + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + reply_code, s_len, = struct.unpack_from('!HB', buf, offset) + offset += 3 + reply_text = buf[offset:offset+s_len] + offset += s_len + s_len, = struct.unpack_from('!B', buf, offset) + offset += 1 + exchange = buf[offset:offset+s_len] + offset += s_len + s_len, = struct.unpack_from('!B', buf, offset) + offset += 1 + routing_key = buf[offset:offset+s_len] + offset += s_len + return BasicReturn(reply_code, reply_text, exchange, routing_key) + + +class BasicReject(AMQPMethodPayload): + """ + Reject an incoming message + + This method allows a client to reject a message. It can be used to interrupt and + cancel large incoming messages, or return untreatable messages to their original + queue. + """ + __slots__ = (u'delivery_tag', u'requeue', ) + + NAME = u'basic.reject' + + INDEX = (60, 90) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x3C\x00\x5A' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = True, False + + IS_SIZE_STATIC = True # this means that argument part has always the same length + IS_CONTENT_STATIC = False # this means that argument part has always the same content + + # See constructor pydoc for details + FIELDS = [ + Field(u'delivery-tag', u'delivery-tag', u'longlong', reserved=False), + Field(u'requeue', u'bit', u'bit', reserved=False), + ] + + def __init__(self, delivery_tag, requeue): + """ + Create frame basic.reject + + :type delivery_tag: int, 64 bit unsigned (delivery-tag in AMQP) + :param requeue: Requeue the message + If requeue is true, the server will attempt to requeue the message. If requeue + is false or the requeue attempt fails the messages are discarded or dead-lettered. + :type requeue: bool (bit in AMQP) + """ + self.delivery_tag = delivery_tag + self.requeue = requeue + + def write_arguments(self, buf): + buf.write(struct.pack('!QB', self.delivery_tag, (self.requeue << 0))) + + def get_size(self): + return 9 + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + delivery_tag, _bit, = struct.unpack_from('!QB', buf, offset) + offset += 8 + requeue = bool(_bit >> 0) + offset += 1 + return BasicReject(delivery_tag, requeue) + + +class BasicRecoverAsync(AMQPMethodPayload): + """ + Redeliver unacknowledged messages + + This method asks the server to redeliver all unacknowledged messages on a + specified channel. Zero or more messages may be redelivered. This method + is deprecated in favour of the synchronous Recover/Recover-Ok. + """ + __slots__ = (u'requeue', ) + + NAME = u'basic.recover-async' + + INDEX = (60, 100) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x3C\x00\x64' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = True, False + + IS_SIZE_STATIC = True # this means that argument part has always the same length + IS_CONTENT_STATIC = False # this means that argument part has always the same content + + # See constructor pydoc for details + FIELDS = [ + Field(u'requeue', u'bit', u'bit', reserved=False), + ] + + def __init__(self, requeue): + """ + Create frame basic.recover-async + + :param requeue: Requeue the message + If this field is zero, the message will be redelivered to the original + recipient. If this bit is 1, the server will attempt to requeue the message, + potentially then delivering it to an alternative subscriber. + :type requeue: bool (bit in AMQP) + """ + self.requeue = requeue + + def write_arguments(self, buf): + buf.write(struct.pack('!B', (self.requeue << 0))) + + def get_size(self): + return 1 + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + _bit, = struct.unpack_from('!B', buf, offset) + offset += 0 + requeue = bool(_bit >> 0) + offset += 1 + return BasicRecoverAsync(requeue) + + +class BasicRecover(AMQPMethodPayload): + """ + Redeliver unacknowledged messages + + This method asks the server to redeliver all unacknowledged messages on a + specified channel. Zero or more messages may be redelivered. This method + replaces the asynchronous Recover. + """ + __slots__ = (u'requeue', ) + + NAME = u'basic.recover' + + INDEX = (60, 110) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x3C\x00\x6E' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = True, False + + IS_SIZE_STATIC = True # this means that argument part has always the same length + IS_CONTENT_STATIC = False # this means that argument part has always the same content + + # See constructor pydoc for details + FIELDS = [ + Field(u'requeue', u'bit', u'bit', reserved=False), + ] + + def __init__(self, requeue): + """ + Create frame basic.recover + + :param requeue: Requeue the message + If this field is zero, the message will be redelivered to the original + recipient. If this bit is 1, the server will attempt to requeue the message, + potentially then delivering it to an alternative subscriber. + :type requeue: bool (bit in AMQP) + """ + self.requeue = requeue + + def write_arguments(self, buf): + buf.write(struct.pack('!B', (self.requeue << 0))) + + def get_size(self): + return 1 + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + _bit, = struct.unpack_from('!B', buf, offset) + offset += 0 + requeue = bool(_bit >> 0) + offset += 1 + return BasicRecover(requeue) + + +class BasicRecoverOk(AMQPMethodPayload): + """ + Confirm recovery + + This method acknowledges a Basic.Recover method. + """ + __slots__ = () + + NAME = u'basic.recover-ok' + + INDEX = (60, 111) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x3C\x00\x6F' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = False, True + + IS_SIZE_STATIC = True # this means that argument part has always the same length + IS_CONTENT_STATIC = True # this means that argument part has always the same content + STATIC_CONTENT = b'\x00\x00\x00\x04\x00\x3C\x00\x6F\xCE' # spans LENGTH, CLASS ID, METHOD ID, ....., FRAME_END + + def __init__(self): + """ + Create frame basic.recover-ok + """ + + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + return BasicRecoverOk() + + +class Tx(AMQPClass): + """ + The tx class allows publish and ack operations to be batched into atomic + + units of work. The intention is that all publish and ack requests issued + within a transaction will complete successfully or none of them will. + Servers SHOULD implement atomic transactions at least where all publish + or ack requests affect a single queue. Transactions that cover multiple + queues may be non-atomic, given that queues can be created and destroyed + asynchronously, and such events do not form part of any transaction. + Further, the behaviour of transactions with respect to the immediate and + mandatory flags on Basic.Publish methods is not defined. + """ + NAME = u'tx' + INDEX = 90 + + +class TxCommit(AMQPMethodPayload): + """ + Commit the current transaction + + This method commits all message publications and acknowledgments performed in + the current transaction. A new transaction starts immediately after a commit. + """ + __slots__ = () + + NAME = u'tx.commit' + + INDEX = (90, 20) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x5A\x00\x14' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = True, False + + IS_SIZE_STATIC = True # this means that argument part has always the same length + IS_CONTENT_STATIC = True # this means that argument part has always the same content + STATIC_CONTENT = b'\x00\x00\x00\x04\x00\x5A\x00\x14\xCE' # spans LENGTH, CLASS ID, METHOD ID, ....., FRAME_END + + def __init__(self): + """ + Create frame tx.commit + """ + + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + return TxCommit() + + +class TxCommitOk(AMQPMethodPayload): + """ + Confirm a successful commit + + This method confirms to the client that the commit succeeded. Note that if a commit + fails, the server raises a channel exception. + """ + __slots__ = () + + NAME = u'tx.commit-ok' + + INDEX = (90, 21) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x5A\x00\x15' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = False, True + + IS_SIZE_STATIC = True # this means that argument part has always the same length + IS_CONTENT_STATIC = True # this means that argument part has always the same content + STATIC_CONTENT = b'\x00\x00\x00\x04\x00\x5A\x00\x15\xCE' # spans LENGTH, CLASS ID, METHOD ID, ....., FRAME_END + + def __init__(self): + """ + Create frame tx.commit-ok + """ + + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + return TxCommitOk() + + +class TxRollback(AMQPMethodPayload): + """ + Abandon the current transaction + + This method abandons all message publications and acknowledgments performed in + the current transaction. A new transaction starts immediately after a rollback. + Note that unacked messages will not be automatically redelivered by rollback; + if that is required an explicit recover call should be issued. + """ + __slots__ = () + + NAME = u'tx.rollback' + + INDEX = (90, 30) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x5A\x00\x1E' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = True, False + + IS_SIZE_STATIC = True # this means that argument part has always the same length + IS_CONTENT_STATIC = True # this means that argument part has always the same content + STATIC_CONTENT = b'\x00\x00\x00\x04\x00\x5A\x00\x1E\xCE' # spans LENGTH, CLASS ID, METHOD ID, ....., FRAME_END + + def __init__(self): + """ + Create frame tx.rollback + """ + + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + return TxRollback() + + +class TxRollbackOk(AMQPMethodPayload): + """ + Confirm successful rollback + + This method confirms to the client that the rollback succeeded. Note that if an + rollback fails, the server raises a channel exception. + """ + __slots__ = () + + NAME = u'tx.rollback-ok' + + INDEX = (90, 31) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x5A\x00\x1F' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = False, True + + IS_SIZE_STATIC = True # this means that argument part has always the same length + IS_CONTENT_STATIC = True # this means that argument part has always the same content + STATIC_CONTENT = b'\x00\x00\x00\x04\x00\x5A\x00\x1F\xCE' # spans LENGTH, CLASS ID, METHOD ID, ....., FRAME_END + + def __init__(self): + """ + Create frame tx.rollback-ok + """ + + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + return TxRollbackOk() + + +class TxSelect(AMQPMethodPayload): + """ + Select standard transaction mode + + This method sets the channel to use standard transactions. The client must use this + method at least once on a channel before using the Commit or Rollback methods. + """ + __slots__ = () + + NAME = u'tx.select' + + INDEX = (90, 10) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x5A\x00\x0A' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = True, False + + IS_SIZE_STATIC = True # this means that argument part has always the same length + IS_CONTENT_STATIC = True # this means that argument part has always the same content + STATIC_CONTENT = b'\x00\x00\x00\x04\x00\x5A\x00\x0A\xCE' # spans LENGTH, CLASS ID, METHOD ID, ....., FRAME_END + + def __init__(self): + """ + Create frame tx.select + """ + + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + return TxSelect() + + +class TxSelectOk(AMQPMethodPayload): + """ + Confirm transaction mode + + This method confirms to the client that the channel was successfully set to use + standard transactions. + """ + __slots__ = () + + NAME = u'tx.select-ok' + + INDEX = (90, 11) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x5A\x00\x0B' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = False, True + + IS_SIZE_STATIC = True # this means that argument part has always the same length + IS_CONTENT_STATIC = True # this means that argument part has always the same content + STATIC_CONTENT = b'\x00\x00\x00\x04\x00\x5A\x00\x0B\xCE' # spans LENGTH, CLASS ID, METHOD ID, ....., FRAME_END + + def __init__(self): + """ + Create frame tx.select-ok + """ + + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + return TxSelectOk() + + +class Confirm(AMQPClass): + """ + The confirm class allows publishers to put the channel in + + confirm mode and subsequently be notified when messages have been + handled by the broker. The intention is that all messages + published on a channel in confirm mode will be acknowledged at + some point. By acknowledging a message the broker assumes + responsibility for it and indicates that it has done something + it deems reasonable with it. + Unroutable mandatory or immediate messages are acknowledged + right after the Basic.Return method. Messages are acknowledged + when all queues to which the message has been routed + have either delivered the message and received an + acknowledgement (if required), or enqueued the message (and + persisted it if required). + Published messages are assigned ascending sequence numbers, + starting at 1 with the first Confirm.Select method. The server + confirms messages by sending Basic.Ack methods referring to these + sequence numbers. + """ + NAME = u'confirm' + INDEX = 85 + + +class ConfirmSelect(AMQPMethodPayload): + """ + This method sets the channel to use publisher acknowledgements. + + The client can only use this method on a non-transactional + channel. + """ + __slots__ = (u'nowait', ) + + NAME = u'confirm.select' + + INDEX = (85, 10) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x55\x00\x0A' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = True, False + + IS_SIZE_STATIC = True # this means that argument part has always the same length + IS_CONTENT_STATIC = False # this means that argument part has always the same content + + # See constructor pydoc for details + FIELDS = [ + Field(u'nowait', u'bit', u'bit', reserved=False), + ] + + def __init__(self, nowait): + """ + Create frame confirm.select + + :param nowait: If set, the server will not respond to the method. the client should + not wait for a reply method. If the server could not complete the + method it will raise a channel or connection exception. + :type nowait: bool (bit in AMQP) + """ + self.nowait = nowait + + def write_arguments(self, buf): + buf.write(struct.pack('!B', (self.nowait << 0))) + + def get_size(self): + return 1 + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + _bit, = struct.unpack_from('!B', buf, offset) + offset += 0 + nowait = bool(_bit >> 0) + offset += 1 + return ConfirmSelect(nowait) + + +class ConfirmSelectOk(AMQPMethodPayload): + """ + This method confirms to the client that the channel was successfully + + set to use publisher acknowledgements. + """ + __slots__ = () + + NAME = u'confirm.select-ok' + + INDEX = (85, 11) # (Class ID, Method ID) + BINARY_HEADER = b'\x00\x55\x00\x0B' # CLASS ID + METHOD ID + + SENT_BY_CLIENT, SENT_BY_SERVER = False, True + + IS_SIZE_STATIC = True # this means that argument part has always the same length + IS_CONTENT_STATIC = True # this means that argument part has always the same content + STATIC_CONTENT = b'\x00\x00\x00\x04\x00\x55\x00\x0B\xCE' # spans LENGTH, CLASS ID, METHOD ID, ....., FRAME_END + + def __init__(self): + """ + Create frame confirm.select-ok + """ + + + @staticmethod + def from_buffer(buf, start_offset): + offset = start_offset + return ConfirmSelectOk() + + +IDENT_TO_METHOD = { + (90, 21): TxCommitOk, + (60, 100): BasicRecoverAsync, + (10, 11): ConnectionStartOk, + (60, 40): BasicPublish, + (60, 50): BasicReturn, + (40, 21): ExchangeDeleteOk, + (20, 20): ChannelFlow, + (40, 31): ExchangeBindOk, + (60, 21): BasicConsumeOk, + (10, 21): ConnectionSecureOk, + (90, 30): TxRollback, + (90, 10): TxSelect, + (85, 11): ConfirmSelectOk, + (10, 61): ConnectionUnblocked, + (50, 11): QueueDeclareOk, + (60, 70): BasicGet, + (90, 11): TxSelectOk, + (10, 30): ConnectionTune, + (60, 11): BasicQosOk, + (60, 80): BasicAck, + (20, 21): ChannelFlowOk, + (60, 60): BasicDeliver, + (90, 31): TxRollbackOk, + (60, 20): BasicConsume, + (85, 10): ConfirmSelect, + (20, 40): ChannelClose, + (60, 71): BasicGetOk, + (50, 30): QueuePurge, + (10, 31): ConnectionTuneOk, + (10, 40): ConnectionOpen, + (60, 30): BasicCancel, + (50, 50): QueueUnbind, + (40, 10): ExchangeDeclare, + (10, 50): ConnectionClose, + (20, 10): ChannelOpen, + (20, 41): ChannelCloseOk, + (60, 110): BasicRecover, + (60, 90): BasicReject, + (50, 31): QueuePurgeOk, + (50, 40): QueueDelete, + (40, 20): ExchangeDelete, + (50, 20): QueueBind, + (10, 41): ConnectionOpenOk, + (60, 120): BasicNack, + (60, 31): BasicCancelOk, + (90, 20): TxCommit, + (10, 10): ConnectionStart, + (60, 10): BasicQos, + (40, 11): ExchangeDeclareOk, + (10, 51): ConnectionCloseOk, + (40, 51): ExchangeUnbindOk, + (20, 11): ChannelOpenOk, + (60, 72): BasicGetEmpty, + (40, 30): ExchangeBind, + (60, 111): BasicRecoverOk, + (40, 40): ExchangeUnbind, + (10, 20): ConnectionSecure, + (50, 41): QueueDeleteOk, + (50, 51): QueueUnbindOk, + (50, 21): QueueBindOk, + (10, 60): ConnectionBlocked, + (50, 10): QueueDeclare, +} + + +BINARY_HEADER_TO_METHOD = { + b'\x00\x5A\x00\x15': TxCommitOk, + b'\x00\x3C\x00\x64': BasicRecoverAsync, + b'\x00\x0A\x00\x0B': ConnectionStartOk, + b'\x00\x3C\x00\x28': BasicPublish, + b'\x00\x3C\x00\x32': BasicReturn, + b'\x00\x28\x00\x15': ExchangeDeleteOk, + b'\x00\x14\x00\x14': ChannelFlow, + b'\x00\x28\x00\x1F': ExchangeBindOk, + b'\x00\x3C\x00\x15': BasicConsumeOk, + b'\x00\x0A\x00\x15': ConnectionSecureOk, + b'\x00\x5A\x00\x1E': TxRollback, + b'\x00\x5A\x00\x0A': TxSelect, + b'\x00\x55\x00\x0B': ConfirmSelectOk, + b'\x00\x0A\x00\x3D': ConnectionUnblocked, + b'\x00\x32\x00\x0B': QueueDeclareOk, + b'\x00\x3C\x00\x46': BasicGet, + b'\x00\x5A\x00\x0B': TxSelectOk, + b'\x00\x0A\x00\x1E': ConnectionTune, + b'\x00\x3C\x00\x0B': BasicQosOk, + b'\x00\x3C\x00\x50': BasicAck, + b'\x00\x14\x00\x15': ChannelFlowOk, + b'\x00\x3C\x00\x3C': BasicDeliver, + b'\x00\x5A\x00\x1F': TxRollbackOk, + b'\x00\x3C\x00\x14': BasicConsume, + b'\x00\x55\x00\x0A': ConfirmSelect, + b'\x00\x14\x00\x28': ChannelClose, + b'\x00\x3C\x00\x47': BasicGetOk, + b'\x00\x32\x00\x1E': QueuePurge, + b'\x00\x0A\x00\x1F': ConnectionTuneOk, + b'\x00\x0A\x00\x28': ConnectionOpen, + b'\x00\x3C\x00\x1E': BasicCancel, + b'\x00\x32\x00\x32': QueueUnbind, + b'\x00\x28\x00\x0A': ExchangeDeclare, + b'\x00\x0A\x00\x32': ConnectionClose, + b'\x00\x14\x00\x0A': ChannelOpen, + b'\x00\x14\x00\x29': ChannelCloseOk, + b'\x00\x3C\x00\x6E': BasicRecover, + b'\x00\x3C\x00\x5A': BasicReject, + b'\x00\x32\x00\x1F': QueuePurgeOk, + b'\x00\x32\x00\x28': QueueDelete, + b'\x00\x28\x00\x14': ExchangeDelete, + b'\x00\x32\x00\x14': QueueBind, + b'\x00\x0A\x00\x29': ConnectionOpenOk, + b'\x00\x3C\x00\x78': BasicNack, + b'\x00\x3C\x00\x1F': BasicCancelOk, + b'\x00\x5A\x00\x14': TxCommit, + b'\x00\x0A\x00\x0A': ConnectionStart, + b'\x00\x3C\x00\x0A': BasicQos, + b'\x00\x28\x00\x0B': ExchangeDeclareOk, + b'\x00\x0A\x00\x33': ConnectionCloseOk, + b'\x00\x28\x00\x33': ExchangeUnbindOk, + b'\x00\x14\x00\x0B': ChannelOpenOk, + b'\x00\x3C\x00\x48': BasicGetEmpty, + b'\x00\x28\x00\x1E': ExchangeBind, + b'\x00\x3C\x00\x6F': BasicRecoverOk, + b'\x00\x28\x00\x28': ExchangeUnbind, + b'\x00\x0A\x00\x14': ConnectionSecure, + b'\x00\x32\x00\x29': QueueDeleteOk, + b'\x00\x32\x00\x33': QueueUnbindOk, + b'\x00\x32\x00\x15': QueueBindOk, + b'\x00\x0A\x00\x3C': ConnectionBlocked, + b'\x00\x32\x00\x0A': QueueDeclare, +} + + +CLASS_ID_TO_CONTENT_PROPERTY_LIST = { + 60: BasicContentPropertyList, +} + +# Methods that are sent as replies to other methods, ie. ConnectionOpenOk: ConnectionOpen +# if a method is NOT a reply, it will not be in this dict +# a method may be a reply for AT MOST one method +REPLY_REASONS_FOR = { + BasicGetEmpty: BasicGet, + BasicGetOk: BasicGet, + ExchangeDeleteOk: ExchangeDelete, + TxSelectOk: TxSelect, + QueueBindOk: QueueBind, + BasicConsumeOk: BasicConsume, + BasicCancelOk: BasicCancel, + TxRollbackOk: TxRollback, + TxCommitOk: TxCommit, + ChannelOpenOk: ChannelOpen, + QueueDeleteOk: QueueDelete, + ExchangeUnbindOk: ExchangeUnbind, + ExchangeBindOk: ExchangeBind, + ChannelCloseOk: ChannelClose, + BasicQosOk: BasicQos, + ConnectionStartOk: ConnectionStart, + QueueUnbindOk: QueueUnbind, + ConfirmSelectOk: ConfirmSelect, + ConnectionCloseOk: ConnectionClose, + QueuePurgeOk: QueuePurge, + QueueDeclareOk: QueueDeclare, + ExchangeDeclareOk: ExchangeDeclare, + ConnectionTuneOk: ConnectionTune, + ConnectionSecureOk: ConnectionSecure, + ConnectionOpenOk: ConnectionOpen, + ChannelFlowOk: ChannelFlow, +} + +# Methods that are replies for other, ie. ConnectionOpenOk: ConnectionOpen +# a method may be a reply for ONE or NONE other methods +# if a method has no replies, it will have an empty list as value here +REPLIES_FOR= { + BasicGetEmpty: [], + BasicRecoverOk: [], + BasicReturn: [], + QueueDeclare: [QueueDeclareOk], + BasicGetOk: [], + ConnectionSecure: [ConnectionSecureOk], + ConnectionTune: [ConnectionTuneOk], + TxRollback: [TxRollbackOk], + TxSelectOk: [], + QueueBindOk: [], + ChannelFlow: [ChannelFlowOk], + BasicConsumeOk: [], + BasicConsume: [BasicConsumeOk], + BasicRecover: [], + BasicCancelOk: [], + ConfirmSelect: [ConfirmSelectOk], + BasicGet: [BasicGetOk, BasicGetEmpty], + TxRollbackOk: [], + QueueBind: [QueueBindOk], + ExchangeDelete: [ExchangeDeleteOk], + BasicAck: [], + ConnectionClose: [ConnectionCloseOk], + ChannelOpenOk: [], + QueueDeleteOk: [], + ExchangeUnbindOk: [], + ConnectionStart: [ConnectionStartOk], + BasicQos: [BasicQosOk], + QueueUnbind: [QueueUnbindOk], + BasicQosOk: [], + BasicReject: [], + ExchangeBindOk: [], + ChannelCloseOk: [], + ExchangeDeclare: [ExchangeDeclareOk], + ConnectionBlocked: [], + BasicPublish: [], + ExchangeUnbind: [ExchangeUnbindOk], + ExchangeDeleteOk: [], + BasicNack: [], + ConnectionStartOk: [], + ExchangeBind: [ExchangeBindOk], + QueueDelete: [QueueDeleteOk], + ConfirmSelectOk: [], + ConnectionCloseOk: [], + QueuePurge: [QueuePurgeOk], + QueueUnbindOk: [], + ChannelOpen: [ChannelOpenOk], + ChannelClose: [ChannelCloseOk], + QueuePurgeOk: [], + QueueDeclareOk: [], + BasicCancel: [BasicCancelOk], + ExchangeDeclareOk: [], + TxCommitOk: [], + ConnectionTuneOk: [], + ConnectionSecureOk: [], + ConnectionUnblocked: [], + ConnectionOpenOk: [], + ChannelFlowOk: [], + BasicRecoverAsync: [], + TxSelect: [TxSelectOk], + BasicDeliver: [], + TxCommit: [TxCommitOk], + ConnectionOpen: [ConnectionOpenOk], +} diff --git a/coolamqp/framing/extensions.py b/coolamqp/framing/extensions.py new file mode 100644 index 0000000000000000000000000000000000000000..355ed0fc9ce824327970ff7786abef24d4133fc5 --- /dev/null +++ b/coolamqp/framing/extensions.py @@ -0,0 +1,5 @@ +# coding=UTF-8 +from __future__ import absolute_import, division, print_function +"""Extension definitions""" + +PUBLISHER_CONFIRMS = b'publisher_confirms' \ No newline at end of file diff --git a/coolamqp/framing/field_table.py b/coolamqp/framing/field_table.py new file mode 100644 index 0000000000000000000000000000000000000000..b53faa44c86b51039fb799b337a2694a2c29379c --- /dev/null +++ b/coolamqp/framing/field_table.py @@ -0,0 +1,188 @@ +# coding=UTF-8 +""" +That funny type, field-table... + +A field-value is of form (value::any, type::char) + +An array is of form [field-value1, field-value2, ...] + +A table is of form ( (name1::bytes, fv1), (name2::bytes, fv2), ...) + +""" +from __future__ import absolute_import, division, print_function +import struct +import six + + +def enframe_decimal(buf, v): # convert decimal to bytes + dps = 0 + for k in six.moves.xrange(20): + k = v * (10 ** dps) + if abs(k-int(k)) < 0.00001: # epsilon + return buf.write(struct.pack('!BI', dps, k)) + + raise ValueError('Could not convert %s to decimal', v) + + +def deframe_decimal(buf, offset): + scale, val = struct.unpack_from('!BI', buf, offset) + return val / (10 ** scale), 5 + + +def deframe_shortstr(buf, offset): # -> value, bytes_eaten + ln, = struct.unpack_from('!B', buf, offset) + return buf[offset+1:offset+1+ln], 1+ln + + +def enframe_shortstr(buf, value): + buf.write(struct.pack('!B', len(value))) + buf.write(value) + + +def deframe_longstr(buf, offset): # -> value, bytes_eaten + ln, = struct.unpack_from('!I', buf, offset) + return buf[offset+4:offset+4+ln], 4 + ln + + +def enframe_longstr(buf, value): + buf.write(struct.pack('!I', len(value))) + buf.write(value) + + +FIELD_TYPES = { + # length, struct, (option)to_bytes (callable(buffer, value)), + # (option)from_bytes (callable(buffer, offset) -> value, bytes_consumed), + # (option)get_len (callable(value) -> length in bytes) + 't': (1, '!?'), # boolean + 'b': (1, '!b'), + 'B': (1, '!B'), + 'U': (2, '!H'), + 'u': (2, '!h'), + 'I': (4, '!I'), + 'i': (4, '!i'), + 'L': (8, '!Q'), + 'l': (8, '!q'), + 'f': (4, '!f'), + 'd': (8, '!d'), + 'D': (5, None, enframe_decimal, deframe_decimal), # decimal-value + 's': (None, None, enframe_shortstr, deframe_shortstr, lambda val: len(val)+1), # shortstr + 'S': (None, None, enframe_longstr, deframe_longstr, lambda val: len(val)+4), # longstr + 'T': (8, '!Q'), + 'V': (0, None, lambda buf, v: None, lambda buf, ofs: None, 0), # rendered as None +} + + +def enframe_field_value(buf, fv): + value, type = fv + buf.write(type) + + opt = FIELD_TYPES[type] + + if opt[1] is not None: + buf.write(struct.pack(opt[1], value)) + else: + opt[2](buf, value) + + +def deframe_field_value(buf, offset): # -> (value, type), bytes_consumed + start_offset = offset + field_type = buf[offset] + offset += 1 + + if field_type not in FIELD_TYPES.keys(): + raise ValueError('Unknown field type %s!', (repr(field_type),)) + + opt = FIELD_TYPES[field_type] + + if opt[1] is not None: + field_val, = struct.unpack_from(FIELD_TYPES[field_type][1], buf, offset) + offset += opt[0] + else: + field_val, delta = opt[3](buf, offset) + offset += delta + + return (field_val, field_type), offset - start_offset + + +def deframe_array(buf, offset): + start_offset = offset + ln, = struct.unpack_from('!I', buf, offset) + offset += 4 + + values = [] + while offset < (start_offset+1+ln): + v, t, delta = deframe_field_value(buf, offset) + offset += delta + values.append((v,t)) + + if offset != start_offset+4+ln: + raise ValueError('Array longer than expected, took %s, expected %s bytes', + (offset-(start_offset+ln+4), ln+4)) + + return values, ln+4 + + +def enframe_array(buf, array): + buf.write(struct.pack('!I', frame_array_size(array)-4)) + for fv in array: + enframe_field_value(buf, fv) + + +def enframe_table(buf, table): + """ + Write AMQP table to buffer + :param buf: + :param table: + :return: + """ + buf.write(struct.pack('!I', frame_table_size(table)-4)) + + for name, fv in table: + buf.write(struct.pack('!B', len(name))) + buf.write(name) + enframe_field_value(buf, fv) + + +def deframe_table(buf, start_offset): # -> (table, bytes_consumed) + """:return: tuple (table, bytes consumed)""" + offset = start_offset + table_length, = struct.unpack_from('!L', buf, start_offset) + offset += 4 + + # we will check if it's really so. + fields = [] + + while offset < (start_offset+table_length+4): + field_name, ln = deframe_shortstr(buf, offset) + offset += ln + fv, delta = deframe_field_value(buf, offset) + offset += delta + fields.append((field_name, fv)) + + if offset > (start_offset+table_length+4): + raise ValueError('Table turned out longer than expected! Found %s bytes expected %s', + (offset-start_offset, table_length)) + + return fields, table_length+4 + + +def frame_field_value_size(fv): + v,t=fv + if FIELD_TYPES[t][0] is None: + return FIELD_TYPES[t][4](v) + 1 + else: + return FIELD_TYPES[t][0] + 1 + + +def frame_array_size(array): + return 4 + sum(frame_field_value_size(fv) for fv in array) + + +def frame_table_size(table): + """:return: length of table representation, in bytes, INCLUDING length header""" + + return 4 + sum(1 + len(k) + frame_field_value_size(fv) for k, fv in table) + + +FIELD_TYPES['A'] = (None, None, enframe_array, deframe_array, frame_array_size) +FIELD_TYPES['F'] = (None, None, enframe_table, deframe_table, frame_table_size) diff --git a/coolamqp/framing/frames.py b/coolamqp/framing/frames.py new file mode 100644 index 0000000000000000000000000000000000000000..f45fe5ee263384d9290364a00770ecd440cc21ae --- /dev/null +++ b/coolamqp/framing/frames.py @@ -0,0 +1,127 @@ +# coding=UTF-8 +""" +Concrete frame definitions +""" +from __future__ import absolute_import, division, print_function + +import struct +import six + +from coolamqp.framing.base import AMQPFrame +from coolamqp.framing.definitions import FRAME_METHOD, FRAME_HEARTBEAT, FRAME_BODY, FRAME_HEADER, FRAME_END, \ + IDENT_TO_METHOD, CLASS_ID_TO_CONTENT_PROPERTY_LIST + + +class AMQPMethodFrame(AMQPFrame): + FRAME_TYPE = FRAME_METHOD + + def __init__(self, channel, payload): + """ + :param channel: channel ID + :param payload: AMQPMethodPayload instance + """ + AMQPFrame.__init__(self, channel) + self.payload = payload + + def write_to(self, buf): + if self.payload.IS_CONTENT_STATIC: + buf.write(struct.pack('!BH', FRAME_METHOD, self.channel)) + buf.write(self.payload.STATIC_CONTENT) + else: + buf.write(struct.pack('!BHL', FRAME_METHOD, self.channel, + 4 + self.payload.get_size())) + buf.write(self.payload.BINARY_HEADER) + self.payload.write_arguments(buf) + buf.write(chr(FRAME_END)) + + @staticmethod + def unserialize(channel, payload_as_buffer): + clsmet = struct.unpack_from('!HH', payload_as_buffer, 0) + + try: + method_payload_class = IDENT_TO_METHOD[clsmet] + payload = method_payload_class.from_buffer(payload_as_buffer, 4) + except KeyError: + raise ValueError('Invalid class %s method %s' % clsmet) + else: + return AMQPMethodFrame(channel, payload) + + def get_size(self): + # frame_header = (method(1) + channel(2) + length(4) + class(2) + method(2) + payload(N) + frame_end(1)) + return 12 + self.payload.get_size() + + +class AMQPHeaderFrame(AMQPFrame): + FRAME_TYPE = FRAME_HEADER + + def __init__(self, channel, class_id, weight, body_size, properties): + """ + :param channel: channel ID + :param class_id: class ID + :param weight: weight (lol wut?) + :param body_size: size of the body to follow + :param properties: a suitable AMQPContentPropertyList instance + """ + AMQPFrame.__init__(self, channel) + self.class_id = class_id + self.weight = weight + self.body_size = body_size + self.properties = properties + + def write_to(self, buf): + buf.write(struct.pack('!BHLHHQ', FRAME_HEADER, self.channel, + 12+self.properties.get_size(), self.class_id, 0, self.body_size)) + self.properties.write_to(buf) + buf.write(chr(FRAME_END)) + + @staticmethod + def unserialize(channel, payload_as_buffer): + # payload starts with class ID + class_id, weight, body_size = struct.unpack_from('!HHQ', payload_as_buffer, 0) + properties = CLASS_ID_TO_CONTENT_PROPERTY_LIST[class_id].from_buffer(payload_as_buffer, 12) + return AMQPHeaderFrame(channel, class_id, weight, body_size, properties) + + def get_size(self): + # frame header is always 7, frame end is 1, content header is 12 + props + return 20 + self.properties.get_size() + + +class AMQPBodyFrame(AMQPFrame): + FRAME_TYPE = FRAME_BODY + + FRAME_SIZE_WITHOUT_PAYLOAD = 8 + + def __init__(self, channel, data): + """ + :type data: binary + """ + AMQPFrame.__init__(self, channel) + assert isinstance(data, (six.binary_type, buffer, memoryview)) + self.data = data + + def write_to(self, buf): + buf.write(struct.pack('!BHL', FRAME_BODY, self.channel, len(self.data))) + buf.write(self.data) + buf.write(chr(FRAME_END)) + + @staticmethod + def unserialize(channel, payload_as_buffer): + return AMQPBodyFrame(channel, payload_as_buffer) + + def get_size(self): + return 8 + len(self.data) + + +class AMQPHeartbeatFrame(AMQPFrame): + FRAME_TYPE = FRAME_HEARTBEAT + LENGTH = 8 + DATA = struct.pack('!BHLB', FRAME_HEARTBEAT, 0, 0, FRAME_END) + + def __init__(self): + AMQPFrame.__init__(self, 0) + + def write_to(self, buf): + buf.write(AMQPHeartbeatFrame.DATA) + + def get_size(self): + return AMQPHeartbeatFrame.LENGTH diff --git a/coolamqp/handler.py b/coolamqp/handler.py deleted file mode 100644 index 1c32fde861bd7ba11eb1fc819d5fdbceb40189d9..0000000000000000000000000000000000000000 --- a/coolamqp/handler.py +++ /dev/null @@ -1,294 +0,0 @@ -# coding=UTF-8 -import threading -from six.moves import queue -import six -import logging -import collections -import time -from .backends import ConnectionFailedError, RemoteAMQPError, Cancelled -from .messages import Exchange -from .events import ConnectionUp, ConnectionDown, ConsumerCancelled, MessageReceived -from .orders import SendMessage, DeclareExchange, ConsumeQueue, CancelQueue, \ - AcknowledgeMessage, NAcknowledgeMessage, DeleteQueue, \ - DeleteExchange, SetQoS, DeclareQueue - -logger = logging.getLogger(__name__) - - -class _ImOuttaHere(Exception): - """Thrown upon thread terminating. - Thrown only if complete_remaining_upon_termination is False""" - - -class ClusterHandlerThread(threading.Thread): - """ - Thread that does bookkeeping for a Cluster. - """ - def __init__(self, cluster): - """ - :param cluster: coolamqp.Cluster - """ - threading.Thread.__init__(self) - - self.cluster = cluster - self.daemon = True # if you don't explicitly wait for me, that means you don't need to - self.is_terminating = False - self.complete_remaining_upon_termination = False - self.order_queue = collections.deque() # queue for inbound orders - self.event_queue = queue.Queue() # queue for tasks done - self.connect_id = -1 # connectID of current connection - - self.declared_exchanges = {} # declared exchanges, by their names - self.queues_by_consumer_tags = {} # tuple of (subbed queue, no_ack::bool), by consumer tags - - self.backend = None - self.first_connect = True - - self.qos = None # or tuple (prefetch_size, prefetch_count) if QoS set - - def _reconnect_attempt(self): - """Single attempt to regain connectivity. May raise ConnectionFailedError""" - self.backend = None - if self.backend is not None: - self.backend.shutdown() - self.backend = None - - self.connect_id += 1 - node = six.next(self.cluster.node_to_connect_to) - logger.info('Connecting to %s', node) - - self.backend = self.cluster.backend(node, self) - - if self.qos is not None: - pre_siz, pre_cou, glob = self.qos - self.backend.basic_qos(pre_siz, pre_cou, glob) - - for exchange in self.declared_exchanges.values(): - self.backend.exchange_declare(exchange) - - failed_queues = [] - for queue, no_ack in self.queues_by_consumer_tags.values(): - while True: - try: - self.backend.queue_declare(queue) - if queue.exchange is not None: - self.backend.queue_bind(queue, queue.exchange) - self.backend.basic_consume(queue, no_ack=no_ack) - logger.info('Consuming from %s no_ack=%s', queue, no_ack) - except RemoteAMQPError as e: - if e.code in (403, 405): # access refused, resource locked - # Ok, queue, what should we do? - if queue.locked_after_reconnect == 'retry': - time.sleep(0.1) - continue # retry until works - elif queue.locked_after_reconnect == 'cancel': - self.event_queue.put(ConsumerCancelled(queue, ConsumerCancelled.REFUSED_ON_RECONNECT)) - failed_queues.append(queue) - elif queue.locked_after_reconnect == 'defer': - self.order_queue.append(ConsumeQueue(queue, no_ack=no_ack)) - failed_queues.append(queue) - else: - raise Exception('wtf') - else: - raise # idk - break - - for failed_queue in failed_queues: - del self.queues_by_consumer_tags[failed_queue.consumer_tag] - - def _reconnect(self): - """Regain connectivity to cluster. May block for a very long time, - as it will not """ - exponential_backoff_delay = 1 - - while not self.cluster.connected: - try: - self._reconnect_attempt() - except ConnectionFailedError as e: - # a connection failure happened :( - logger.warning('Connecting failed due to %s while connecting and initial setup', repr(e)) - self.cluster.connected = False - if self.backend is not None: - self.backend.shutdown() - self.backend = None # good policy to release resources before you sleep - time.sleep(exponential_backoff_delay) - - if self.is_terminating and (not self.complete_remaining_upon_termination): - raise _ImOuttaHere() - - exponential_backoff_delay = min(60, exponential_backoff_delay * 2) - else: - logger.info('Connected to AMQP broker via %s', self.backend) - self.cluster.connected = True - self.event_queue.put(ConnectionUp(initial=self.first_connect)) - self.first_connect = False - - - def perform_order(self): - order = self.order_queue.popleft() - - try: - if order.cancelled: - logger.debug('Order %s was cancelled', order) - order._failed(Cancelled()) - return - - if isinstance(order, SendMessage): - self.backend.basic_publish(order.message, order.exchange, order.routing_key) - elif isinstance(order, SetQoS): - self.qos = order.qos - pre_siz, pre_cou, glob = order.qos - self.backend.basic_qos(pre_siz, pre_cou, glob) - elif isinstance(order, DeclareExchange): - self.backend.exchange_declare(order.exchange) - self.declared_exchanges[order.exchange.name] = order.exchange - elif isinstance(order, DeleteExchange): - self.backend.exchange_delete(order.exchange) - if order.exchange.name in self.declared_exchanges: - del self.declared_exchanges[order.exchange.name] - elif isinstance(order, DeclareQueue): - self.backend.queue_declare(order.queue) - elif isinstance(order, DeleteQueue): - self.backend.queue_delete(order.queue) - elif isinstance(order, ConsumeQueue): - if order.queue.consumer_tag in self.queues_by_consumer_tags: - order._completed() - return # already consuming, belay that - - self.backend.queue_declare(order.queue) - - if order.queue.exchange is not None: - self.backend.queue_bind(order.queue, order.queue.exchange) - - self.backend.basic_consume(order.queue, no_ack=order.no_ack) - self.queues_by_consumer_tags[order.queue.consumer_tag] = order.queue, order.no_ack - elif isinstance(order, CancelQueue): - try: - q, no_ack = self.queues_by_consumer_tags.pop(order.queue.consumer_tag) - except KeyError: - pass # wat? - else: - self.backend.basic_cancel(order.queue.consumer_tag) - self.event_queue.put(ConsumerCancelled(order.queue, ConsumerCancelled.USER_CANCEL)) - elif isinstance(order, AcknowledgeMessage): - if order.connect_id == self.connect_id: - self.backend.basic_ack(order.delivery_tag) - elif isinstance(order, NAcknowledgeMessage): - if order.connect_id == self.connect_id: - self.backend.basic_reject(order.delivery_tag) - except RemoteAMQPError as e: - logger.error('Remote AMQP error: %s', e) - order._failed(e) # we are allowed to go on - except ConnectionFailedError as e: - logger.error('Connection failed while %s: %s', order, e) - self.order_queue.appendleft(order) - raise - else: - order._completed() - - def __run_wrap(self): # throws _ImOuttaHere - # Loop while there are things to do - while (not self.is_terminating) or (len(self.order_queue) > 0): - try: - while len(self.order_queue) > 0: - self.perform_order() - - # just drain shit - self.backend.process(max_time=0.05) - except ConnectionFailedError as e: - logger.warning('Connection to broker lost: %s', e) - self.cluster.connected = False - self.event_queue.put(ConnectionDown()) - - # =========================== remove SendMessagees with discard_on_fail - my_orders = [] # because order_queue is used by many threads - while len(self.order_queue) > 0: - order = self.order_queue.popleft() - if isinstance(order, SendMessage): - if order.message.discard_on_fail: - order._discard() - continue - - my_orders.append(order) - - # Ok, we have them in order of execution. Append-left in reverse order - # to preserve previous order - for order in reversed(my_orders): - my_orders.appendleft(order) - - self._reconnect() - - def run(self): - try: - self._reconnect() - self.__run_wrap() - except _ImOuttaHere: - pass - - assert self.is_terminating - if self.cluster.connected or (self.backend is not None): - if self.backend is not None: - self.backend.shutdown() - self.backend = None - - self.cluster.connected = False - - def terminate(self): - """ - Called by Cluster. Tells to finish all jobs and quit. - Unacked messages will not be acked. If this is called, connection may die at any time. - """ - self.is_terminating = True - - ## events called - def _on_recvmessage(self, body, exchange_name, routing_key, delivery_tag, properties): - """ - Upon receiving a message - """ - from .messages import ReceivedMessage - - self.event_queue.put(MessageReceived(ReceivedMessage(body, self, - self.connect_id, - exchange_name, - routing_key, - properties, - delivery_tag=delivery_tag))) - - def _on_consumercancelled(self, consumer_tag): - """ - A consumer has been cancelled - """ - try: - queue, no_ack = self.queues_by_consumer_tags.pop(consumer_tag) - except KeyError: - return # what? - - self.event_queue.put(ConsumerCancelled(queue, ConsumerCancelled.BROKER_CANCEL)) - - ## methods to enqueue something into CHT to execute - - def _do_ackmessage(self, receivedMessage, on_completed=None): - """ - Order acknowledging a message. - :param receivedMessage: a ReceivedMessage object to ack - :param on_completed: callable/0 to call when acknowledgemenet succeeded - :return: an AcknowledgeMess - """ - a = AcknowledgeMessage(receivedMessage.connect_id, - receivedMessage.delivery_tag, - on_completed=on_completed) - self.order_queue.append(a) - return a - - - def _do_nackmessage(self, receivedMessage, on_completed=None): - """ - Order acknowledging a message. - :param receivedMessage: a ReceivedMessage object to ack - :param on_completed: callable/0 to call when acknowledgemenet succeeded - """ - a = NAcknowledgeMessage(receivedMessage.connect_id, - receivedMessage.delivery_tag, - on_completed=on_completed) - self.order_queue.append(a) - return a diff --git a/coolamqp/messages.py b/coolamqp/messages.py deleted file mode 100644 index 2a2dde2f04b6bcf4f16ddfbf3dacff8ae1fcca25..0000000000000000000000000000000000000000 --- a/coolamqp/messages.py +++ /dev/null @@ -1,134 +0,0 @@ -# coding=UTF-8 -import uuid -import six - - -class Message(object): - """AMQP message object""" - - def __init__(self, body, properties=None): - """ - Create a Message object. - - Please take care with passing empty bodies, as py-amqp has some failure on it. - - :param body: stream of octets - :type body: str (py2) or bytes (py3) - :param properties: AMQP properties to be sent along - """ - if isinstance(body, six.text_type): - raise TypeError('body cannot be a text type!') - self.body = six.binary_type(body) - self.properties = properties or {} - - -class ReceivedMessage(Message): - """Message as received from AMQP system""" - - def __init__(self, body, cht, connect_id, exchange_name, routing_key, properties=None, delivery_tag=None): - """ - :param body: message body. A stream of octets. - :type body: str (py2) or bytes (py3) - :param cht: parent ClusterHandlerThread that emitted this message - :param connect_id: connection ID. ClusterHandlerThread will check this in order - not to ack messages that were received from a dead connection - :param exchange_name: name of exchange this message was submitted to - :param routing_key: routing key with which this message was sent - :param properties: dictionary. Headers received from AMQP or None for empty dict - - :param delivery_tag: delivery tag assigned by AMQP broker to confirm this message. - leave None if auto-ack - """ - Message.__init__(self, body, properties=properties) - - self.cht = cht - self.connect_id = connect_id - self.delivery_tag = delivery_tag - self.exchange_name = exchange_name - self.routing_key = routing_key - - def nack(self, on_completed=None): - """ - Negative-acknowledge this message to the broker. - - This internally results in a basic.reject - - :param on_completed: callable/0 to call on acknowledged. Callable will be executed in - ClusterHandlerThread's context. - :return: an Order, that can ve waited upon for a result - """ - return self.cht._do_nackmessage(self, on_completed=on_completed) - - def ack(self, on_completed=None): - """ - Acknowledge this message to the broker. - :param on_completed: callable/0 to call on acknowledged. Callable will be executed in - ClusterHandlerThread's context. - :return: an Order, that can ve waited upon for a result - """ - return self.cht._do_ackmessage(self, on_completed=on_completed) - - -class Exchange(object): - """ - This represents an Exchange used in AMQP. - This is hashable. - """ - - direct = None # the direct exchange - - def __init__(self, name='', type='direct', durable=True, auto_delete=False): - self.name = name - self.type = type - self.durable = durable - self.auto_delete = auto_delete - - def __hash__(self): - return self.name.__hash__() - - def __eq__(self, other): - return self.name == other.name - -Exchange.direct = Exchange() - - -class Queue(object): - """ - This object represents a Queue that applications consume from or publish to. - - Caveat: Please note the locked_after_reconnect option in constructor - """ - - def __init__(self, name='', durable=False, exchange=None, exclusive=False, auto_delete=False, - locked_after_reconnect='retry'): - """ - Create a queue definition. - - :param name: name of the queue. - Take special care if this is empty. If empty, this will be filled-in by the broker - upon declaration. If a disconnect happens, and connection to other node is - reestablished, this name will CHANGE AGAIN, and be reflected in this object. - This change will be done before CoolAMQP signals reconnection. - :param durable: Is the queue durable? - :param exchange: Exchange for this queue to bind to. None for no binding. - :param exclusive: Is this queue exclusive? - :param auto_delete: Is this queue auto_delete ? - :param locked_after_reconnect: Behaviour when queue is exclusive and ACCESS_REFUSED/RESOURCE_LOCKED - is seen on reconnect. Because broker might not know that we have failed, 'retry' will - try again until succeeds (default option). This might block for a long time, until the broker - realizes previous connection is dead and deletes the queue. - 'cancel' will return a ConsumerCancelled to client - 'defer' will attempt to configure the queue later, but will not block other tasks from progressing. - """ - self.name = name - # if name is '', this will be filled in with broker-generated name upon declaration - self.durable = durable - self.exchange = exchange - self.auto_delete = auto_delete - self.exclusive = exclusive - - self.anonymous = name == '' # if this queue is anonymous, it must be regenerated upon reconnect - - self.consumer_tag = name if name != '' else uuid.uuid4().hex # consumer tag to use in AMQP comms - self.locked_after_reconnect = locked_after_reconnect - assert locked_after_reconnect in ('retry', 'cancel', 'defer') \ No newline at end of file diff --git a/coolamqp/objects.py b/coolamqp/objects.py new file mode 100644 index 0000000000000000000000000000000000000000..dda8ea5f1033a15b731971ff1120c228b239773f --- /dev/null +++ b/coolamqp/objects.py @@ -0,0 +1,274 @@ +# coding=UTF-8 +""" +Core objects used in CoolAMQP +""" +import threading +import uuid +import six +import logging +import concurrent.futures + +from coolamqp.framing.definitions import BasicContentPropertyList as MessageProperties + +__all__ = ('Message', 'ReceivedMessage', 'MessageProperties', 'Queue', 'Exchange', 'Future') + +logger = logging.getLogger(__name__) + +EMPTY_PROPERTIES = MessageProperties() + + +class Message(object): + """ + An AMQP message. Has a binary body, and some properties. + + Properties is a highly regularized class - see coolamqp.framing.definitions.BasicContentPropertyList + for a list of possible properties. + """ + + Properties = MessageProperties # an alias for easier use + + def __init__(self, body, properties=None): + """ + Create a Message object. + + Please take care with passing empty bodies, as py-amqp has some failure on it. + + :param body: stream of octets + :type body: str (py2) or bytes (py3) + :param properties: AMQP properties to be sent along. + default is 'no properties at all' + You can pass a dict - it will be passed to MessageProperties, + but it's slow - don't do that. + :type properties: MessageProperties instance, None or a dict + """ + if isinstance(body, six.text_type): + raise TypeError('body cannot be a text type!') + self.body = six.binary_type(body) + + if isinstance(properties, dict): + self.properties = MessageProperties(**properties) + elif properties is None: + self.properties = EMPTY_PROPERTIES + else: + self.properties = properties + + +LAMBDA_NONE = lambda: None + +class ReceivedMessage(Message): + """ + A message that was received from the AMQP broker. + + It additionally has an exchange name, routing key used, it's delivery tag, + and methods for ack() or nack(). + + Note that if the consumer that generated this message was no_ack, .ack() and .nack() are no-ops. + """ + + def __init__(self, body, exchange_name, routing_key, + properties=None, + delivery_tag=None, + ack=None, + nack=None): + """ + :param body: message body. A stream of octets. + :type body: str (py2) or bytes (py3) + :param cht: parent ClusterHandlerThread that emitted this message + :param connect_id: connection ID. ClusterHandlerThread will check this in order + not to ack messages that were received from a dead connection + :param exchange_name: name of exchange this message was submitted to + :param routing_key: routing key with which this message was sent + :param properties: a suitable BasicContentPropertyList subinstance + + :param delivery_tag: delivery tag assigned by AMQP broker to confirm this message + :param ack: a callable to call when you want to ack (via basic.ack) this message. None if received + by the no-ack mechanism + :param nack: a callable to call when you want to nack (via basic.reject) this message. None if received + by the no-ack mechanism + """ + Message.__init__(self, body, properties=properties) + + self.delivery_tag = delivery_tag + self.exchange_name = exchange_name + self.routing_key = routing_key + + self.ack = ack or LAMBDA_NONE + self.nack = nack or LAMBDA_NONE + + +class Exchange(object): + """ + This represents an Exchange used in AMQP. + This is hashable. + """ + + direct = None # the direct exchange + + def __init__(self, name='', type='direct', durable=True, auto_delete=False): + self.name = name + self.type = type + self.durable = durable + self.auto_delete = auto_delete + + def __hash__(self): + return self.name.__hash__() + + def __eq__(self, other): + return self.name == other.name + +Exchange.direct = Exchange() + + +class Queue(object): + """ + This object represents a Queue that applications consume from or publish to. + """ + + def __init__(self, name=u'', durable=False, exchange=None, exclusive=False, auto_delete=False): + """ + Create a queue definition. + + :param name: name of the queue. + Take special care if this is empty. If empty, this will be filled-in by the broker + upon declaration. If a disconnect happens, and connection to other node is + reestablished, this name will CHANGE AGAIN, and be reflected in this object. + This change will be done before CoolAMQP signals reconnection. + :param durable: Is the queue durable? + :param exchange: Exchange for this queue to bind to. None for no binding. + :param exclusive: Is this queue exclusive? + :param auto_delete: Is this queue auto_delete ? + """ + self.name = name.encode('utf8') + # if name is '', this will be filled in with broker-generated name upon declaration + self.durable = durable + self.exchange = exchange + self.auto_delete = auto_delete + self.exclusive = exclusive + + self.anonymous = name == '' # if this queue is anonymous, it must be regenerated upon reconnect + + self.consumer_tag = name if name != '' else uuid.uuid4().hex # consumer tag to use in AMQP comms + + def __eq__(self, other): + return self.name == other.name + + def __hash__(self): + return hash(self.name) + + +class Future(concurrent.futures.Future): + """ + Future returned by asynchronous CoolAMQP methods. + + A strange future (only one thread may wait for it) + """ + __slots__ = ('lock', 'completed', 'successfully', '_result', 'running', 'callables', 'cancelled') + + + def __init__(self): + self.lock = threading.Lock() + self.lock.acquire() + + self.completed = False + self.successfully = None + self._result = None + self.cancelled = False + self.running = True + + self.callables = [] + + def add_done_callback(self, fn): + self.callables.append(fn) + + def result(self, timeout=None): + assert timeout is None, u'Non-none timeouts not supported' + self.lock.acquire() + + if self.completed: + if self.successfully: + return self._result + else: + raise self._result + else: + if self.cancelled: + raise concurrent.futures.CancelledError() + else: + # it's invalid to release the lock, not do the future if it's not cancelled + raise RuntimeError(u'Invalid state!') + + def cancel(self): + """ + When cancelled, future will attempt not to complete (completed=False). + :return: + """ + self.cancelled = True + + def __finish(self, result, successful): + self.completed = True + self.successfully = successful + self._result = result + self.lock.release() + + for callable in self.callables: + try: + callable(self) + except Exception as e: + logger.error('Exception in base order future: %s', repr(e)) + except BaseException as e: + logger.critical('WILD NASAL DEMON APPEARED: %s', repr(e)) + + def set_result(self, result=None): + self.__finish(result, True) + + def set_exception(self, exception): + self.__finish(exception, False) + + def set_cancel(self): + """Executor has seen that this is cancelled, and discards it from list of things to do""" + assert self.cancelled + self.completed = False + self.lock.release() + + +class NodeDefinition(object): + """ + Definition of a reachable AMQP node. + + This object is hashable. + """ + + def __init__(self, *args, **kwargs): + """ + Create a cluster node definition. + + a = ClusterNode(host='192.168.0.1', user='admin', password='password', + virtual_host='vhost') + + or + + a = ClusterNode('192.168.0.1', 'admin', 'password') + + Additional keyword parameters that can be specified: + heartbeat - heartbeat interval in seconds + port - TCP port to use. Default is 5672 + """ + + self.heartbeat = kwargs.pop('heartbeat', None) + self.port = kwargs.pop('port', 5672) + + if len(kwargs) > 0: + # Prepare arguments for amqp.connection.Connection + self.host = kwargs['host'] + self.user = kwargs['user'] + self.password = kwargs['password'] + self.virtual_host = kwargs.get('virtual_host', '/') + elif len(args) == 3: + self.host, self.user, self.password = args + self.virtual_host = '/' + elif len(args) == 4: + self.host, self.user, self.password, self.virtual_host = args + else: + raise NotImplementedError #todo implement this + + def __str__(self): + return six.text_type(b'amqp://%s:%s@%s/%s'.encode('utf8') % (self.host, self.port, self.user, self.virtual_host)) \ No newline at end of file diff --git a/coolamqp/orders.py b/coolamqp/orders.py deleted file mode 100644 index 2c97858213765d5d217c37277d50ec8c467b7e61..0000000000000000000000000000000000000000 --- a/coolamqp/orders.py +++ /dev/null @@ -1,175 +0,0 @@ -# coding=UTF-8 -""" -Orders that can be dispatched to ClusterHandlerThread -""" -from threading import Lock -import warnings - - -_NOOP_COMP = lambda: None -_NOOP_FAIL = lambda e: None - - -class Order(object): - """Base class for orders dispatched to ClusterHandlerThread""" - def __init__(self, on_completed=None, on_failed=None): - """ - Please note that callbacks will be executed BEFORE the lock is released, - but after .result is updated, ie. if - you have something like - - amqp.send(.., on_completed=hello).result() - bye() - - then hello() will be called BEFORE bye(). - Callbacks are called from CoolAMQP's internal thread. - - If this fails, then property .error_code can be read to get the error code. - and .reply_text has the reply of the server or some other reason. These are set before - callbacks are called. - - Error code is None, if not available, or AMQP constants describing errors, - eg. 502 for syntax error. - - A discarded or cancelled order is considered FAILED - """ - self.on_completed = on_completed or _NOOP_COMP - self.on_failed = on_failed or _NOOP_FAIL - self._result = None # None on non-completed - # True on completed OK - # exception instance on failed - # private - self.lock = Lock() - self.lock.acquire() - self.cancelled = False #: public - self.discarded = False #: public - self.error_code = None - self.reply_text = None - - def has_finished(self): - """Return if this task has either completed or failed""" - return self._result is not None - - def cancel(self): - """Cancel this order""" - self.cancelled = True - - def _completed(self): # called by handler - self._result = True - self.on_completed() - self.lock.release() - - def _discard(self): # called by handler - from coolamqp.backends.base import Discarded - self.discarded = True - self.on_failed(Discarded()) - self.lock.release() - - def _failed(self, e): # called by handler - """ - :param e: AMQPError instance or Cancelled instance - """ - from coolamqp.backends import Cancelled - self._result = e - if not isinstance(e, Cancelled): # a true error - self.error_code = e.code - self.reply_text = e.reply_text - - self.on_failed(e) - self.lock.release() - - def wait(self): - """Wait until this is completed and return whether the order succeeded""" - self.lock.acquire() - return self._result is True - - def has_failed(self): - """Return whether the operation failed, ie. completed but with an error code. - Cancelled and discarded ops are considered failed. - This assumes that this order has been .wait()ed upon""" - return self._result is True - - def result(self): - """Wait until this is completed and return a response""" - warnings.warn('Use .wait() instead', PendingDeprecationWarning) - self.lock.acquire() - return self._result - - @staticmethod - def _discarded(on_completed=None, on_failed=None): # return order for a discarded message - o = Order(on_completed=on_completed, on_failed=on_failed) - self.on_completed() - - -class SendMessage(Order): - """Send a message""" - def __init__(self, message, exchange, routing_key, discard_on_fail=False, on_completed=None, on_failed=None): - Order.__init__(self, on_completed=on_completed, on_failed=on_failed) - self.message = message - self.exchange = exchange - self.discard_on_fail = discard_on_fail - self.routing_key = routing_key - - -class _Exchange(Order): - """Things with exchanges""" - def __init__(self, exchange, on_completed=None, on_failed=None): - Order.__init__(self, on_completed=on_completed, on_failed=on_failed) - self.exchange = exchange - - -class DeclareExchange(_Exchange): - """Declare an exchange""" - - -class DeleteExchange(_Exchange): - """Delete an exchange""" - - -class _Queue(Order): - """Things with queues""" - def __init__(self, queue, on_completed=None, on_failed=None): - Order.__init__(self, on_completed=on_completed, on_failed=on_failed) - self.queue = queue - - -class DeclareQueue(_Queue): - """Declare a a queue""" - - -class ConsumeQueue(_Queue): - """Declare and consume from a queue""" - def __init__(self, queue, no_ack=False, on_completed=None, on_failed=None): - _Queue.__init__(self, queue, on_completed=on_completed, on_failed=on_failed) - self.no_ack = no_ack - - -class DeleteQueue(_Queue): - """Delete a queue""" - - -class CancelQueue(_Queue): - """Cancel consuming from a queue""" - - -class SetQoS(Order): - """Set QoS""" - def __init__(self, prefetch_window, prefetch_count, global_, on_completed=None, on_failed=None): - Order.__init__(self, on_completed=on_completed, on_failed=on_failed) - self.qos = prefetch_window, prefetch_count, global_ -1 - -class _AcksAndNacks(Order): - """related to acking and nacking""" - def __init__(self, connect_id, delivery_tag, on_completed): - Order.__init__(self, on_completed=on_completed) - self.connect_id = connect_id - self.delivery_tag = delivery_tag - - -class AcknowledgeMessage(_AcksAndNacks): - """ACK a message""" - - -class NAcknowledgeMessage(_AcksAndNacks): - """NACK a message""" diff --git a/coolamqp/uplink/__init__.py b/coolamqp/uplink/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..cef792f6a97f9813867f7ca9741d5f1783ee03bd --- /dev/null +++ b/coolamqp/uplink/__init__.py @@ -0,0 +1,18 @@ +# coding=UTF-8 +""" + +Core object here is Connection. This package: + - establishes basic connectivity (up to the point where you can open channels yourself) + - takes care of heartbeats + +You can wait for a particular frame by setting watches on connections. +Watches will fire upon an event triggering them. + +EVERYTHING HERE IS CALLED BY LISTENER THREAD UNLESS STATED OTHERWISE. + +""" +from __future__ import absolute_import, division, print_function + +from coolamqp.uplink.connection import Connection, HeaderOrBodyWatch, MethodWatch, AnyWatch, FailWatch +from coolamqp.uplink.listener import ListenerThread +from coolamqp.uplink.handshake import PUBLISHER_CONFIRMS, CONSUMER_CANCEL_NOTIFY diff --git a/coolamqp/uplink/connection/__init__.py b/coolamqp/uplink/connection/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..7ec0d01c5042462dcf11aa52740f21c667e1f0ae --- /dev/null +++ b/coolamqp/uplink/connection/__init__.py @@ -0,0 +1,16 @@ +# coding=UTF-8 +""" +Comprehensive management of a framing connection. + +Connection is something that can: + - call something when an AMQPFrame is received + - send AMQPFrame's + + Pretty much CoolAMQP is about persistent "attaches" that attach to transient connection + (they die when down) to do stuff, ie. send messages, consume, etc. +""" +from __future__ import absolute_import, division, print_function + +from coolamqp.uplink.connection.connection import Connection +from coolamqp.uplink.connection.watches import FailWatch, Watch, HeaderOrBodyWatch, MethodWatch, AnyWatch +from coolamqp.uplink.connection.states import ST_OFFLINE, ST_CONNECTING, ST_ONLINE diff --git a/coolamqp/uplink/connection/connection.py b/coolamqp/uplink/connection/connection.py new file mode 100644 index 0000000000000000000000000000000000000000..426804257177c6cb953ab37af963d96a4af7848f --- /dev/null +++ b/coolamqp/uplink/connection/connection.py @@ -0,0 +1,301 @@ +# coding=UTF-8 +from __future__ import absolute_import, division, print_function +import logging +import collections +import time +import socket +import six + +from coolamqp.uplink.connection.recv_framer import ReceivingFramer +from coolamqp.uplink.connection.send_framer import SendingFramer +from coolamqp.framing.frames import AMQPMethodFrame +from coolamqp.uplink.handshake import Handshaker +from coolamqp.framing.definitions import ConnectionClose, ConnectionCloseOk +from coolamqp.uplink.connection.watches import MethodWatch +from coolamqp.uplink.connection.states import ST_ONLINE, ST_OFFLINE, ST_CONNECTING + + +logger = logging.getLogger(__name__) + + +class Connection(object): + """ + An object that manages a connection in a comprehensive way. + + It allows for sending and registering watches for particular things. Watch will + listen for eg. frame on particular channel, frame on any channel, or connection teardown. + Watches will also get a callback for connection being non-operational (eg. torn down). + + WARNING: Thread-safety of watch operation hinges on atomicity + of .append and .pop. + + Lifecycle of connection is such: + + Connection created -> state is ST_CONNECTING + .start() called -> state is ST_CONNECTING + connection.open-ok -> state is ST_ONLINE + """ + + def __init__(self, node_definition, listener_thread): + """ + Create an object that links to an AMQP broker. + + No data will be physically sent until you hit .start() + + :param node_definition: NodeDefinition instance to use + :param listener_thread: ListenerThread to use as async engine + """ + self.listener_thread = listener_thread + self.node_definition = node_definition + + self.recvf = ReceivingFramer(self.on_frame) + + self.watches = {} # channel => list of [Watch instance] + self.any_watches = [] # list of Watches that should check everything + + self.finalizers = [] + + + self.state = ST_CONNECTING + + self.callables_on_connected = [] # list of callable/0 + + # Negotiated connection parameters - handshake will fill this in + self.free_channels = [] # attaches can use this for shit. + # WARNING: thread safety of this hinges on atomicity of .pop or .append + self.frame_max = None + self.heartbeat = None + self.extensions = [] + + def call_on_connected(self, callable): + """ + Register a callable to be called when this links to the server. + + If you call it while the connection IS up, callable will be called even before this returns. + + You should be optimally an attached attache to receive this. + + :param callable: callable/0 to call + """ + if self.state == ST_ONLINE: + callable() + else: + self.callables_on_connected.append(callable) + + def on_connected(self): + """Called by handshaker upon reception of final connection.open-ok""" + print(self.free_channels) + self.state = ST_ONLINE + + while len(self.callables_on_connected) > 0: + self.callables_on_connected.pop()() + + def start(self): + """ + Start processing events for this connect. Create the socket, + transmit 'AMQP\x00\x00\x09\x01' and roll. + + Warning: This will block for as long as the TCP connection setup takes. + """ + + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + while True: + try: + sock.connect((self.node_definition.host, self.node_definition.port)) + except socket.error as e: + print(e) + time.sleep(0.5) # Connection refused? Very bad things? + else: + break + + sock.settimeout(0) + sock.send('AMQP\x00\x00\x09\x01') + + Handshaker(self, self.node_definition, self.on_connected) + self.listener_socket = self.listener_thread.register(sock, + on_read=self.recvf.put, + on_fail=self.on_fail) + self.sendf = SendingFramer(self.listener_socket.send) + self.watch_for_method(0, (ConnectionClose, ConnectionCloseOk), self.on_connection_close) + + def add_finalizer(self, callable): + """ + Add a callable to be executed when all watches were failed and we're really going down. + + Finalizers are not used for logic stuff, but for situations like making TCP reconnects. + When we are making a reconnect, we need to be sure that all watches fired - so logic is intact. + + DO NOT PUT CALLABLES THAT HAVE TO DO WITH STATE THINGS, ESPECIALLY ATTACHES. + + :param callable: callable/0 + """ + self.finalizers.append(callable) + + def on_fail(self): + """ + Called by event loop when the underlying connection is closed. + + This means the connection is dead, cannot be used anymore, and all operations + running on it now are aborted, null and void. + + This calls fails all registered watches. + Called by ListenerThread. + + WARNING: Note that .on_fail can get called twice - once from .on_connection_close, + and second time from ListenerThread when socket is disposed of + Therefore we need to make sure callbacks are called EXACTLY once + """ + self.state = ST_OFFLINE # Update state + + watchlists = [self.watches[channel] for channel in self.watches] + + for watchlist in watchlists: # Run all watches - failed + for watch in watchlist: + watch.failed() + + for watch in self.any_watches: + watch.failed() + + self.watches = {} # Clear the watch list + self.any_watches = [] + + # call finalizers + while len(self.finalizers) > 0: + self.finalizers.pop()() + + def on_connection_close(self, payload): + """ + Server attempted to close the connection.. or maybe we did? + + Called by ListenerThread. + """ + print('We are GOING DOOOWN') + self.on_fail() # it does not make sense to prolong the agony + + if isinstance(payload, ConnectionClose): + print(payload.reply_code, payload.reply_text) + self.send([AMQPMethodFrame(0, ConnectionCloseOk())]) + elif isinstance(payload, ConnectionCloseOk): + self.send(None) + + def send(self, frames, priority=False): + """ + Schedule to send some frames. + + Take care: This won't stop you from sending frames larger tham frame_max. + Broker will probably close the connection if he sees that. + + :param frames: list of frames or None to close the link + :param reason: optional human-readable reason for this action + """ + if frames is not None: + self.sendf.send(frames, priority=priority) + else: + # Listener socket will kill us when time is right + self.listener_socket.send(None) + + def on_frame(self, frame): + """ + Called by event loop upon receiving an AMQP frame. + + This will verify all watches on given channel if they were hit, + and take appropriate action. + + Unhandled frames will be logged - if they were sent, they probably were important. + + :param frame: AMQPFrame that was received + """ + if isinstance(frame, AMQPMethodFrame): # temporary, for debugging + print('RECEIVED', frame.payload.NAME) + else: + print('RECEIVED ', frame) + + watch_handled = False # True if ANY watch handled this + + # ==================== process per-channel watches + if frame.channel in self.watches: + watches = self.watches[frame.channel] # a list + + alive_watches = [] + while len(watches) > 0: + watch = watches.pop() + + if watch.cancelled: + continue + + watch_triggered = watch.is_triggered_by(frame) + watch_handled |= watch_triggered + + if (not watch_triggered) or (not watch.oneshot): + # Watch remains alive if it was NOT triggered, or it's NOT a oneshot + alive_watches.append(watch) + + for watch in alive_watches: + watches.append(watch) + + # ==================== process "any" watches + alive_watches = [] + while len(self.any_watches): + watch = self.any_watches.pop() + watch_triggered = watch.is_triggered_by(frame) + watch_handled |= watch_triggered + + if (not watch_triggered) or (not watch.oneshot): + # Watch remains alive if it was NOT triggered, or it's NOT a oneshot + alive_watches.append(watch) + + for watch in alive_watches: + self.any_watches.append(watch) + + if not watch_handled: + logger.critical('Unhandled frame %s', frame) + + def watchdog(self, delay, callback): + """ + Call callback in delay seconds. One-shot. + + Shall the connection die in the meantime, watchdog will not + be called, and everything will process according to + ListenerThread's on_fail callback. + """ + self.listener_socket.oneshot(delay, callback) + + def unwatch_all(self, channel_id): + """ + Remove all watches from specified channel + """ + self.watches.pop(channel_id, None) + + def watch(self, watch): + """ + Register a watch. + :param watch: Watch to register + """ + assert self.state != ST_OFFLINE + if watch.channel is None: + self.any_watches.append(watch) + elif watch.channel not in self.watches: + self.watches[watch.channel] = collections.deque([watch]) + else: + self.watches[watch.channel].append(watch) + + def watch_for_method(self, channel, method, callback, on_fail=None): + """ + :param channel: channel to monitor + :param method: AMQPMethodPayload class or tuple of AMQPMethodPayload classes + :param callback: callable(AMQPMethodPayload instance) + """ + mw = MethodWatch(channel, method, callback, on_end=on_fail) + self.watch(mw) + return mw + + def method_and_watch(self, channel_id, method_payload, method_or_methods, callback): + """ + A syntactic sugar for + + .watch_for_method(channel_id, method_or_methdods, callback) + .send([AMQPMethodFrame(channel_id, method_payload)]) + """ + self.watch_for_method(channel_id, method_or_methods, callback) + self.send([AMQPMethodFrame(channel_id, method_payload)]) diff --git a/coolamqp/uplink/connection/recv_framer.py b/coolamqp/uplink/connection/recv_framer.py new file mode 100644 index 0000000000000000000000000000000000000000..1075f52066130fc8c87982e2ef54a0d048546047 --- /dev/null +++ b/coolamqp/uplink/connection/recv_framer.py @@ -0,0 +1,138 @@ +# coding=UTF-8 +from __future__ import absolute_import, division, print_function + +import collections +import io +import six +import struct + +from coolamqp.framing.frames import AMQPBodyFrame, AMQPHeaderFrame, AMQPHeartbeatFrame, AMQPMethodFrame +from coolamqp.framing.definitions import FRAME_HEADER, FRAME_HEARTBEAT, FRAME_END, FRAME_METHOD, FRAME_BODY + +FRAME_TYPES = { + FRAME_HEADER: AMQPHeaderFrame, + FRAME_BODY: AMQPBodyFrame, + FRAME_METHOD: AMQPMethodFrame +} + + +class ReceivingFramer(object): + """ + Assembles AMQP framing from received data. + + Just call with .put(data) upon receiving, + and on_frame will be called with fresh frames. + + Not thread safe. + + State machine + (frame_type is None) and has_bytes(1) -> (frame_type <- bytes(1)) + + (frame_type is HEARTBEAT) and has_bytes(AMQPHeartbeatFrame.LENGTH-1) -> (output_frame, frame_type <- None) + (frame_type is not HEARTBEAT and not None) and has_bytes(6) -> (frame_channel <- bytes(2), + frame_size <- bytes(4)) + + (frame_size is not None) and has_bytes(frame_size+1) -> (output_frame, + frame_type <- None + frame_size < None) + """ + def __init__(self, on_frame=lambda frame: None): + self.chunks = collections.deque() # all received data + self.total_data_len = 0 + + self.frame_type = None + self.frame_channel = None + self.frame_size = None + + self.bytes_needed = None # bytes needed for a new frame + self.on_frame = on_frame + + def put(self, data): + """ + Called upon receiving data. + + May result in any number of .on_frame() calls + :param data: received data + """ + self.total_data_len += len(data) + self.chunks.append(buffer(data)) + + while self._statemachine(): + pass + + def _extract(self, up_to): # return up to up_to bytes from current chunk, switch if necessary + assert self.total_data_len >= up_to, 'Tried to extract %s but %s remaining' % (up_to, self.total_data_len) + if up_to >= len(self.chunks[0]): + q = self.chunks.popleft() + else: + q = buffer(self.chunks[0], 0, up_to) + self.chunks[0] = buffer(self.chunks[0], up_to) + + self.total_data_len -= len(q) + return q + + def _statemachine(self): + # state rule 1 + if self.frame_type is None and self.total_data_len > 0: + self.frame_type = ord(self._extract(1)[0]) + + if self.frame_type not in (FRAME_HEARTBEAT, FRAME_HEADER, FRAME_METHOD, FRAME_BODY): + raise ValueError('Invalid frame') + + return True + + # state rule 2 + elif (self.frame_type == FRAME_HEARTBEAT) and (self.total_data_len >= AMQPHeartbeatFrame.LENGTH-1): + data = b'' + while len(data) < AMQPHeartbeatFrame.LENGTH-1: + data = data + six.binary_type(self._extract(AMQPHeartbeatFrame.LENGTH-1 - len(data))) + + if data != AMQPHeartbeatFrame.DATA[1:]: + # Invalid heartbeat frame! + raise ValueError('Invalid AMQP heartbeat') + + self.on_frame(AMQPHeartbeatFrame()) + self.frame_type = None + + return True + + # state rule 3 + elif (self.frame_type != FRAME_HEARTBEAT) and (self.frame_type is not None) and (self.frame_size is None) and (self.total_data_len > 6): + hdr = b'' + while len(hdr) < 6: + hdr = hdr + six.binary_type(self._extract(6 - len(hdr))) + + self.frame_channel, self.frame_size = struct.unpack('!HI', hdr) + + return True + + # state rule 4 + elif (self.frame_size is not None) and (self.total_data_len >= (self.frame_size+1)): + + if len(self.chunks[0]) >= self.frame_size: + # We can subslice it - it's very fast + payload = self._extract(self.frame_size) + else: + # Construct a separate buffer :( + payload = io.BytesIO() + while payload.tell() < self.frame_size: + payload.write(self._extract(self.frame_size - payload.tell())) + + assert payload.tell() <= self.total_data_len + + payload = buffer(payload.getvalue()) + + if ord(self._extract(1)[0]) != FRAME_END: + raise ValueError('Invalid frame end') + + try: + frame = FRAME_TYPES[self.frame_type].unserialize(self.frame_channel, payload) + except ValueError: + raise + + self.on_frame(frame) + self.frame_type = None + self.frame_size = None + + return True + return False \ No newline at end of file diff --git a/coolamqp/uplink/connection/send_framer.py b/coolamqp/uplink/connection/send_framer.py new file mode 100644 index 0000000000000000000000000000000000000000..bb07e45552b6fe213454b344fdd68805c250b41a --- /dev/null +++ b/coolamqp/uplink/connection/send_framer.py @@ -0,0 +1,50 @@ +# coding=UTF-8 +from __future__ import absolute_import, division, print_function + +import collections +import threading +import io +import socket + + +class SendingFramer(object): + """ + Assembles AMQP framing from received data and orchestrates their upload via a socket. + + Just call with .put(data) and get framing by iterator .framing(). + + Not thread safe. + + State machine + (frame_type is None) and has_bytes(1) -> (frame_type <- bytes(1)) + + (frame_type is HEARTBEAT) and has_bytes(3) -> (output_frame, frame_type <- None) + (frame_type is not HEARTBEAT and not None) and has_bytes(6) -> (frame_channel <- bytes(2), + frame_size <- bytes(4)) + + (frame_size is not None) and has_bytes(frame_size+1) -> (output_frame, + frame_type <- None + frame_size < None) + """ + def __init__(self, on_send=lambda data: None): + """ + :param on_send: a callable(data, priority=False) that can be called with some data to send + data will always be entire AMQP frames! + """ + self.on_send = on_send + + + def send(self, frames, priority=False): + """ + Schedule to send some frames. + :param frames: list of AMQPFrame instances + :param priority: preempty existing frames + """ + length = sum(frame.get_size() for frame in frames) + buf = io.BytesIO(bytearray(length)) + + for frame in frames: + frame.write_to(buf) + + q = buf.getvalue() + self.on_send(q, priority) diff --git a/coolamqp/uplink/connection/states.py b/coolamqp/uplink/connection/states.py new file mode 100644 index 0000000000000000000000000000000000000000..9f340b26b72a615934208b19042aa7c859899d4f --- /dev/null +++ b/coolamqp/uplink/connection/states.py @@ -0,0 +1,6 @@ +# coding=UTF-8 +from __future__ import absolute_import, division, print_function + +ST_OFFLINE = 0 +ST_CONNECTING = 1 +ST_ONLINE = 2 \ No newline at end of file diff --git a/coolamqp/uplink/connection/watches.py b/coolamqp/uplink/connection/watches.py new file mode 100644 index 0000000000000000000000000000000000000000..10026c88df42b88f4a201db3fba7ecb15b924fbd --- /dev/null +++ b/coolamqp/uplink/connection/watches.py @@ -0,0 +1,128 @@ +# coding=UTF-8 +from __future__ import absolute_import, division, print_function + +from coolamqp.framing.frames import AMQPMethodFrame, AMQPHeartbeatFrame, AMQPHeaderFrame, AMQPBodyFrame + + +class Watch(object): + """ + A watch is placed per-channel, to listen for a particular frame. + """ + + def __init__(self, channel, oneshot): + """ + :param channel: Channel to listen to. + all channels if None is passed + :param oneshot: Is destroyed after triggering? + """ + self.channel = channel + self.oneshot = oneshot + self.cancelled = False + + def is_triggered_by(self, frame): + """ + Does frame trigger this watch? + Run callable if it does. + :param frame: AMQPFrame instance + :return: bool + """ + raise Exception('Abstract method') + + def failed(self): + """ + This watch will process things no more, because underlying + link has failed + """ + + def cancel(self): + """ + Called by watch's user. This watch will not receive events anymore + (whether about frame or fail), and it will be discarded upon next iteration. + """ + self.cancelled = True + + +class AnyWatch(Watch): + """ + Watch that listens for any frame. + + It does not listen for failures. + + Used because heartbeating is implemented improperly EVERYWHERE + (ie. you might not get a heartbeat when connection is so loaded it just can't get it in time, + due to loads and loads of message exchanging). + + Eg. RabbitMQ will happily disconnect you if you don't, but it can get lax with heartbeats + as it wants. + """ + def __init__(self, callable): + super(AnyWatch, self).__init__(None, False) + self.callable = callable + + def is_triggered_by(self, frame): + self.callable(frame) + return True + + +class FailWatch(Watch): + """ + A special kind of watch that fires when connection has died + """ + def __init__(self, callable): + super(FailWatch, self).__init__(None, True) + self.callable = callable + + def is_triggered_by(self, frame): + return False + + def failed(self): + """Connection failed!""" + self.callable() + + +class HeaderOrBodyWatch(Watch): + """ + A multi-shot watch listening for AMQP header or body frames + """ + def __init__(self, channel, callable): + Watch.__init__(self, channel, False) + self.callable = callable + + def is_triggered_by(self, frame): + if not (isinstance(frame, (AMQPHeaderFrame, AMQPBodyFrame))): + return False + self.callable(frame) + return True + + +class MethodWatch(Watch): + """ + One-shot watch listening for methods. + """ + def __init__(self, channel, method_or_methods, callable, on_end=None): + """ + :param method_or_methods: class, or list of AMQPMethodPayload classes + :param callable: callable(AMQPMethodPayload instance) + :param on_end: callable/0 on link dying + """ + Watch.__init__(self, channel, True) + self.callable = callable + if isinstance(method_or_methods, (list, tuple)): + self.methods = tuple(method_or_methods) + else: + self.methods = method_or_methods + self.on_end = on_end + + def failed(self): + if self.on_end is not None: + self.on_end() + + def is_triggered_by(self, frame): + + if not isinstance(frame, AMQPMethodFrame): + return False + + if isinstance(frame.payload, self.methods): + self.callable(frame.payload) + return True + return False diff --git a/coolamqp/uplink/handshake.py b/coolamqp/uplink/handshake.py new file mode 100644 index 0000000000000000000000000000000000000000..4c8bf1e6fdfdad717af656fe0b6087387dbc4004 --- /dev/null +++ b/coolamqp/uplink/handshake.py @@ -0,0 +1,113 @@ +# coding=UTF-8 +from __future__ import absolute_import, division, print_function +""" +Provides reactors that can authenticate an AQMP session +""" +import six +from coolamqp.framing.definitions import ConnectionStart, ConnectionStartOk, \ + ConnectionTune, ConnectionTuneOk, ConnectionOpen, ConnectionOpenOk, ConnectionClose +from coolamqp.framing.frames import AMQPMethodFrame +from coolamqp.uplink.connection.states import ST_ONLINE + + +PUBLISHER_CONFIRMS = b'publisher_confirms' +CONSUMER_CANCEL_NOTIFY = b'consumer_cancel_notify' + +SUPPORTED_EXTENSIONS = [ + PUBLISHER_CONFIRMS, + CONSUMER_CANCEL_NOTIFY +] + +CLIENT_DATA = [ + # because RabbitMQ is some kind of a fascist and does not allow + # these fields to be of type short-string + (b'product', (b'CoolAMQP', b'S')), + (b'version', (b'develop', b'S')), + (b'copyright', (b'Copyright (C) 2016-2017 DMS Serwis', b'S')), + (b'information', (b'Licensed under the MIT License.\nSee https://github.com/smok-serwis/coolamqp for details', b'S')), + (b'capabilities', ([(capa, (True, b't')) for capa in SUPPORTED_EXTENSIONS], b'F')), + ] + +WATCHDOG_TIMEOUT = 10 + + +class Handshaker(object): + """ + Object that given a connection rolls the handshake. + """ + + def __init__(self, connection, node_definition, on_success): + """ + :param connection: Connection instance to use + :type node_definition: NodeDefinition + :param on_success: callable/0, on success + """ + self.connection = connection + self.login = node_definition.user.encode('utf8') + self.password = node_definition.password.encode('utf8') + self.virtual_host = node_definition.virtual_host.encode('utf8') + self.heartbeat = node_definition.heartbeat or 0 + self.connection.watch_for_method(0, ConnectionStart, self.on_connection_start) + + # Callbacks + self.on_success = on_success + + # Called by internal setup + def on_watchdog(self): + """ + Called WATCHDOG_TIMEOUT seconds after setup begins + + If we are not ST_ONLINE after that much, something is wrong and pwn this connection. + """ + # Not connected in 20 seconds - abort + if self.connection.state != ST_ONLINE: + # closing the connection this way will get to Connection by channels of ListenerThread + self.connection.send(None) + + def on_connection_start(self, payload): + + sasl_mechanisms = payload.mechanisms.split(b' ') + locale_supported = payload.locales.split(b' ') + + # Select a mechanism + if b'PLAIN' not in sasl_mechanisms: + raise ValueError('Server does not support PLAIN') + + # Select capabilities + server_props = dict(payload.server_properties) + if b'capabilities' in server_props: + for label, fv in server_props[b'capabilities'][0]: + if label in SUPPORTED_EXTENSIONS: + if fv[0]: + self.connection.extensions.append(label) + + self.connection.watchdog(WATCHDOG_TIMEOUT, self.on_watchdog) + self.connection.watch_for_method(0, ConnectionTune, self.on_connection_tune) + self.connection.send([ + AMQPMethodFrame(0, + ConnectionStartOk(CLIENT_DATA, b'PLAIN', + b'\x00' + self.login + b'\x00' + self.password, + locale_supported[0] + )) + ]) + + def on_connection_tune(self, payload): + self.connection.frame_max = payload.frame_max + self.connection.heartbeat = min(payload.heartbeat, self.heartbeat) + print('Selected', payload.channel_max, 'channels') + for channel in six.moves.xrange(1, (65535 if payload.channel_max == 0 else payload.channel_max)+1): + self.connection.free_channels.append(channel) + + self.connection.watch_for_method(0, ConnectionOpenOk, self.on_connection_open_ok) + self.connection.send([ + AMQPMethodFrame(0, ConnectionTuneOk(payload.channel_max, payload.frame_max, self.connection.heartbeat)), + AMQPMethodFrame(0, ConnectionOpen(self.virtual_host)) + ]) + + # Install heartbeat handlers NOW, if necessary + if self.connection.heartbeat > 0: + from coolamqp.uplink.heartbeat import Heartbeater + Heartbeater(self.connection, self.connection.heartbeat) + + def on_connection_open_ok(self, payload): + self.on_success() diff --git a/coolamqp/uplink/heartbeat.py b/coolamqp/uplink/heartbeat.py new file mode 100644 index 0000000000000000000000000000000000000000..3f37ba89a3b1fedc33de7f977b8bed40bf8fde58 --- /dev/null +++ b/coolamqp/uplink/heartbeat.py @@ -0,0 +1,49 @@ +# coding=UTF-8 +from __future__ import absolute_import, division, print_function +import monotonic + +from coolamqp.framing.frames import AMQPHeartbeatFrame +from coolamqp.uplink.connection.watches import AnyWatch + + +class Heartbeater(object): + """ + An object that handles heartbeats + """ + + def __init__(self, connection, heartbeat_interval=0): + self.connection = connection + self.heartbeat_interval = heartbeat_interval + + self.last_heartbeat_on = monotonic.monotonic() # last heartbeat from server + + self.connection.watchdog(self.heartbeat_interval, self.on_timer) + self.connection.watch(AnyWatch(self.on_heartbeat)) + + def on_heartbeat(self, frame): + self.last_heartbeat_on = monotonic.monotonic() + + def on_any_frame(self): + """ + Hehehe, most AMQP servers are not AMQP-compliant. + Consider a situation where you just got like a metric shitton of messages, + and the TCP connection is bustin' filled with those frames. + + Server should still be able to send a heartbeat frame, but it doesn't, because of the queue, and + BANG, dead. + + I know I'm being picky, but at least I implement this behaviour correctly - see priority argument in send. + + Anyway, we should register an all-watch for this. + """ + self.last_heartbeat_on = monotonic.monotonic() + + def on_timer(self): + """Timer says we should send a heartbeat""" + self.connection.send([AMQPHeartbeatFrame()], priority=True) + + if (monotonic.monotonic() - self.last_heartbeat_on) > 2*self.heartbeat_interval: + # closing because of heartbeat + self.connection.send(None) + + self.connection.watchdog(self.heartbeat_interval, self.on_timer) diff --git a/coolamqp/uplink/listener/__init__.py b/coolamqp/uplink/listener/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..6e09bdfd2188ed56b734fcadeee2c4f3879ab522 --- /dev/null +++ b/coolamqp/uplink/listener/__init__.py @@ -0,0 +1,18 @@ +# coding=UTF-8 +""" +A listener is a thread that monitors a bunch of sockets for activity. + +Think "asyncio" but I couldn't be bothered to learn Twisted. + +It provides both for sending and receiving messages. It is written +as a package, because the optimal network call, epoll, is not +available on Windows, and you might just want to use it. + +select and poll are not optimal, because if you wanted to send +something in that small gap where select/poll blocks, you won't +immediately be able to do so. With epoll, you can. +""" +from __future__ import absolute_import, division, print_function + + +from coolamqp.uplink.listener.thread import ListenerThread diff --git a/coolamqp/uplink/listener/epoll_listener.py b/coolamqp/uplink/listener/epoll_listener.py new file mode 100644 index 0000000000000000000000000000000000000000..a27095e0e1cbc46535385b1970afd9e1c29c5f58 --- /dev/null +++ b/coolamqp/uplink/listener/epoll_listener.py @@ -0,0 +1,151 @@ +# coding=UTF-8 +from __future__ import absolute_import, division, print_function +import six +import logging +import select +import monotonic +import socket +import collections +import heapq + +from coolamqp.uplink.listener.socket import SocketFailed, BaseSocket + + +logger = logging.getLogger(__name__) + + +RO = select.EPOLLIN | select.EPOLLHUP | select.EPOLLERR +RW = RO | select.EPOLLOUT + + +class EpollSocket(BaseSocket): + """ + EpollListener substitutes your BaseSockets with this + """ + def __init__(self, sock, on_read, on_fail, listener): + BaseSocket.__init__(self, sock, on_read=on_read, on_fail=on_fail) + self.listener = listener + self.priority_queue = collections.deque() + + def send(self, data, priority=False): + """ + This can actually get called not by ListenerThread. + """ + BaseSocket.send(self, data, priority=priority) + try: + self.listener.epoll.modify(self, RW) + except socket.error: + # silence. If there are errors, it's gonna get nuked soon. + pass + + def oneshot(self, seconds_after, callable): + """ + Set to fire a callable N seconds after + :param seconds_after: seconds after this + :param callable: callable/0 + """ + self.listener.oneshot(self, seconds_after, callable) + + def noshot(self): + """ + Clear all time-delayed callables. + + This will make no time-delayed callables delivered if ran in listener thread + """ + self.listener.noshot(self) + + +class EpollListener(object): + """ + A listener using epoll. + """ + + def __init__(self): + self.epoll = select.epoll() + self.fd_to_sock = {} + self.time_events = [] + + def wait(self, timeout=1): + events = self.epoll.poll(timeout=timeout) + + # Timer events + mono = monotonic.monotonic() + while len(self.time_events) > 0 and (self.time_events[0][0] < mono): + ts, fd, callback = heapq.heappop(self.time_events) + callback() + + for fd, event in events: + sock = self.fd_to_sock[fd] + + # Errors + try: + if event & (select.EPOLLERR | select.EPOLLHUP): + raise SocketFailed() + + if event & select.EPOLLIN: + sock.on_read() + + if event & select.EPOLLOUT: + if sock.on_write(): + # I'm done with sending for now + self.epoll.modify(sock.fileno(), RW) + + except SocketFailed: + self.epoll.unregister(fd) + del self.fd_to_sock[fd] + sock.on_fail() + self.noshot(sock) + sock.close() + + def noshot(self, sock): + """ + Clear all one-shots for a socket + :param sock: BaseSocket instance + """ + fd = sock.fileno() + self.time_events = [q for q in self.time_events if q[1] != fd] + + def shutdown(self): + """ + Forcibly close all sockets that this manages (calling their on_fail's), + and close the object. + + This object is unusable after this call. + """ + for sock in six.itervalues(self.fd_to_sock): + sock.on_fail() + sock.close() + self.fd_to_sock = {} + self.epoll.close() + self.time_events = [] + + def oneshot(self, sock, delta, callback): + """ + A socket registers a time callback + :param sock: BaseSocket instance + :param delta: "this seconds after now" + :param callback: callable/0 + """ + if sock.fileno() in self.fd_to_sock: + heapq.heappush(self.time_events, (monotonic.monotonic() + delta, + sock.fileno(), + callback + )) + + def register(self, sock, on_read=lambda data: None, + on_fail=lambda: None): + """ + Add a socket to be listened for by the loop. + + :param sock: a socket instance (as returned by socket module) + :param on_read: callable(data) to be called with received data + :param on_fail: callable() to be called when socket fails + + :return: a BaseSocket instance to use instead of this socket + """ + sock = EpollSocket(sock, on_read, on_fail, self) + self.fd_to_sock[sock.fileno()] = sock + + self.epoll.register(sock, RW) + return sock + diff --git a/coolamqp/uplink/listener/socket.py b/coolamqp/uplink/listener/socket.py new file mode 100644 index 0000000000000000000000000000000000000000..ffd56372ee8eddb44ad45b938c227060f819de42 --- /dev/null +++ b/coolamqp/uplink/listener/socket.py @@ -0,0 +1,145 @@ +# coding=UTF-8 +from __future__ import absolute_import, division, print_function +import collections +import socket + + +class SocketFailed(IOError): + """Failure during socket operation. It needs to be discarded.""" + + +class BaseSocket(object): + """ + Base class for sockets provided to listeners. + + This is based on a standard TCP socket. + + To be instantiated only by Listeners. + """ + + def __init__(self, sock, on_read=lambda data: None, + on_time=lambda: None, + on_fail=lambda: None): + """ + + :param sock: socketobject + :param on_read: callable(data) to be called when data is read. + Listener thread context + Raises ValueError on socket should be closed + :param on_time: callable() when time provided by socket expires + :param on_fail: callable() when socket is dead and to be discarded. + Listener thread context. + Socket descriptor will be handled by listener. + This should not + """ + assert sock is not None + self.sock = sock + self.data_to_send = collections.deque() + self.priority_queue = collections.deque() # when a piece of data is finished, this queue is checked first + self.my_on_read = on_read + self._on_fail = on_fail + self.on_time = on_time + self.is_failed = False + + def on_fail(self): + self.is_failed = True + self._on_fail() + + def send(self, data, priority=True): + """ + Schedule to send some data. + + :param data: data to send, or None to terminate this socket. + Note that data will be sent atomically, ie. without interruptions. + :param priority: preempt other datas. Property of sending data atomically will be maintained. + """ + if self.is_failed: return + + if data is None: + # THE POPE OF NOPE + self.priority_queue = collections.deque() + self.data_to_send = collections.deque([None]) + return + + if priority: + self.priority_queue.append(data) + else: + self.data_to_send.append(data) + + def oneshot(self, seconds_after, callable): + """ + Set to fire a callable N seconds after + :param seconds_after: seconds after this + :param callable: callable/0 + """ + raise Exception('Abstract; listener should override that') + + def noshot(self): + """ + Clear all time-delayed callables. + + This will make no time-delayed callables delivered if ran in listener thread + """ + raise Exception('Abstract; listener should override that') + + def on_read(self): + """Socket is readable, called by Listener""" + if self.is_failed: return + try: + data = self.sock.recv(2048) + except (IOError, socket.error): + raise SocketFailed() + + if len(data) == 0: + raise SocketFailed() + + try: + self.my_on_read(data) + except ValueError: + raise SocketFailed() + + def on_write(self): + """ + Socket is writable, called by Listener + :raises SocketFailed: on socket error + :return: True if I'm done sending shit for now + """ + if self.is_failed: return + + while True: + if len(self.data_to_send) == 0: + if len(self.priority_queue) == 0: + return True + else: + self.data_to_send.appendleft(self.priority_queue.popleft()) + + assert len(self.data_to_send) > 0 + + if self.data_to_send[0] is None: + raise SocketFailed() # We should terminate the connection! + + try: + sent = self.sock.send(self.data_to_send[0]) + except (IOError, socket.error): + raise SocketFailed() + + if sent < len(self.data_to_send[0]): + # Not everything could be sent + self.data_to_send[0] = buffer(self.data_to_send[0], sent) + return False + else: + # Looks like everything has been sent + self.data_to_send.popleft() # mark as sent + + if len(self.priority_queue) > 0: + # We can send a priority pack + print('Deploying priority data') + self.data_to_send.appendleft(self.priority_queue.popleft()) + + def fileno(self): + """Return descriptor number""" + return self.sock.fileno() + + def close(self): + """Close this socket""" + self.sock.close() \ No newline at end of file diff --git a/coolamqp/uplink/listener/thread.py b/coolamqp/uplink/listener/thread.py new file mode 100644 index 0000000000000000000000000000000000000000..40abd2449db5201e4e5b5b3e3fa26b1db94689b7 --- /dev/null +++ b/coolamqp/uplink/listener/thread.py @@ -0,0 +1,40 @@ +# coding=UTF-8 +from __future__ import absolute_import, division, print_function + +import threading + +from coolamqp.uplink.listener.epoll_listener import EpollListener + + +class ListenerThread(threading.Thread): + """ + A thread that does the listening. + + It automatically picks the best listener for given platform. + """ + + def __init__(self): + threading.Thread.__init__(self) + self.daemon = True + self.terminating = False + self.listener = EpollListener() + + def terminate(self): + self.terminating = True + + def run(self): + while not self.terminating: + self.listener.wait(timeout=1) + self.listener.shutdown() + + def register(self, sock, on_read=lambda data: None, on_fail=lambda: None): + """ + Add a socket to be listened for by the loop. + + :param sock: a socket instance (as returned by socket module) + :param on_read: callable(data) to be called with received data + :param on_fail: callable() to be called when socket fails + + :return: a BaseSocket instance to use instead of this socket + """ + return self.listener.register(sock, on_read, on_fail) diff --git a/examples/send_to_myself.py b/examples/send_to_myself.py deleted file mode 100644 index bc113e54c71fc7be96d3bf035a84a63ee5c5bdb9..0000000000000000000000000000000000000000 --- a/examples/send_to_myself.py +++ /dev/null @@ -1,36 +0,0 @@ -# coding=UTF-8 -from __future__ import print_function -from coolamqp import Cluster, ClusterNode, Queue, Message, ConnectionUp, ConnectionDown, MessageReceived, ConsumerCancelled -import logging -import time - -QUEUE_NAME = 'f' - -logging.basicConfig() - -cluster = Cluster([ClusterNode('192.168.224.31:5672', 'smok', 'smok', 'smok', heartbeat=10)]).start() - -a_queue = Queue(QUEUE_NAME, auto_delete=True) -cluster.consume(a_queue) -cluster.qos(0, 1) - - -q = time.time() -while True: - if time.time() - q > 10: - q = time.time() - cluster.send(Message('hello world'), routing_key=QUEUE_NAME) - - evt = cluster.drain(2) - - if isinstance(evt, ConnectionUp): - print('Connection is up') - elif isinstance(evt, ConnectionDown): - print('Connection is down') - elif isinstance(evt, MessageReceived): - print('Message is %s' % (evt.message.body, )) - evt.message.ack() - elif isinstance(evt, ConsumerCancelled): - print('Consumer %s cancelled' % (evt.queue.name, )) - - diff --git a/requirements.txt b/requirements.txt index 5fecbcdb185943b6108d6983bf76d394d392c227..e791b2aada9c24c75598436e3e21008bda714327 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ amqp six monotonic +futures diff --git a/resources/amqp0-9-1.extended.xml b/resources/amqp0-9-1.extended.xml new file mode 100644 index 0000000000000000000000000000000000000000..7ba2d60d3bab985e47c48fa25e2b219bcbac1786 --- /dev/null +++ b/resources/amqp0-9-1.extended.xml @@ -0,0 +1,3347 @@ +<?xml version = "1.0"?> + +<!-- + WARNING: Modified from the official 0-9-1 specification XML by + the addition of: + confirm.select and confirm.select-ok, + exchange.bind and exchange.bind-ok, + exchange.unbind and exchange.unbind-ok, + basic.nack, + the ability for the Server to send basic.ack, basic.nack and + basic.cancel to the client, and + the un-deprecation of exchange.declare{auto-delete} and exchange.declare{internal} + + Modifications are (c) 2010-2013 VMware, Inc. and may be distributed under + the same BSD license as the stripped spec. +--> + +<!-- + Copyright Notice + ================ + Copyright (c) 2006-2008 Cisco Systems, Credit Suisse, Deutsche Boerse + Systems, Envoy Technologies, Inc., Goldman Sachs, IONA Technologies PLC, + iMatix Corporation, JPMorgan Chase Bank Inc. N.A, Novell, Rabbit + Technologies Ltd., Red Hat, Inc., TWIST Process Innovations Ltd, WS02 + Inc. and 29West Inc. All rights reserved. + + License + ======= + Cisco Systems, Credit Suisse, Deutsche Boerse Systems, Envoy Technologies, + Inc., Goldman Sachs, IONA Technologies PLC, iMatix Corporation, JPMorgan + Chase Bank Inc. N.A, Novell, Rabbit Technologies Ltd., Red Hat, Inc., + TWIST Process Innovations Ltd, WS02, Inc. and 29West Inc. (collectively, + the "Authors") each hereby grants to you a worldwide, perpetual, + royalty-free, nontransferable, nonexclusive license to (i) copy, display, + distribute and implement the Advanced Messaging Queue Protocol ("AMQP") + Specification and (ii) the Licensed Claims that are held by the Authors, + all for the purpose of implementing the Advanced Messaging Queue Protocol + Specification. Your license and any rights under this Agreement will + terminate immediately without notice from any Author if you bring any + claim, suit, demand, or action related to the Advanced Messaging Queue + Protocol Specification against any Author. Upon termination, you shall + destroy all copies of the Advanced Messaging Queue Protocol Specification + in your possession or control. + + As used hereunder, "Licensed Claims" means those claims of a patent or + patent application, throughout the world, excluding design patents and + design registrations, owned or controlled, or that can be sublicensed + without fee and in compliance with the requirements of this Agreement, + by an Author or its affiliates now or at any future time and which would + necessarily be infringed by implementation of the Advanced Messaging + Queue Protocol Specification. A claim is necessarily infringed hereunder + only when it is not possible to avoid infringing it because there is no + plausible non-infringing alternative for implementing the required + portions of the Advanced Messaging Queue Protocol Specification. + Notwithstanding the foregoing, Licensed Claims shall not include any + claims other than as set forth above even if contained in the same patent + as Licensed Claims; or that read solely on any implementations of any + portion of the Advanced Messaging Queue Protocol Specification that are + not required by the Advanced Messaging Queue ProtocolSpecification, or + that, if licensed, would require a payment of royalties by the licensor + to unaffiliated third parties. Moreover, Licensed Claims shall not + include (i) any enabling technologies that may be necessary to make or + use any Licensed Product but are not themselves expressly set forth in + the Advanced Messaging Queue Protocol Specification (e.g., semiconductor + manufacturing technology, compiler technology, object oriented + technology, networking technology, operating system technology, and the + like); or (ii) the implementation of other published standards developed + elsewhere and merely referred to in the body of the Advanced Messaging + Queue Protocol Specification, or (iii) any Licensed Product and any + combinations thereof the purpose or function of which is not required + for compliance with the Advanced Messaging Queue Protocol Specification. + For purposes of this definition, the Advanced Messaging Queue Protocol + Specification shall be deemed to include both architectural and + interconnection requirements essential for interoperability and may also + include supporting source code artifacts where such architectural, + interconnection requirements and source code artifacts are expressly + identified as being required or documentation to achieve compliance with + the Advanced Messaging Queue Protocol Specification. + + As used hereunder, "Licensed Products" means only those specific portions + of products (hardware, software or combinations thereof) that implement + and are compliant with all relevant portions of the Advanced Messaging + Queue Protocol Specification. + + The following disclaimers, which you hereby also acknowledge as to any + use you may make of the Advanced Messaging Queue Protocol Specification: + + THE ADVANCED MESSAGING QUEUE PROTOCOL SPECIFICATION IS PROVIDED "AS IS," + AND THE AUTHORS MAKE NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR + IMPLIED, INCLUDING, BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, OR TITLE; THAT THE + CONTENTS OF THE ADVANCED MESSAGING QUEUE PROTOCOL SPECIFICATION ARE + SUITABLE FOR ANY PURPOSE; NOR THAT THE IMPLEMENTATION OF THE ADVANCED + MESSAGING QUEUE PROTOCOL SPECIFICATION WILL NOT INFRINGE ANY THIRD PARTY + PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS. + + THE AUTHORS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL, + INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF OR RELATING TO ANY + USE, IMPLEMENTATION OR DISTRIBUTION OF THE ADVANCED MESSAGING QUEUE + PROTOCOL SPECIFICATION. + + The name and trademarks of the Authors may NOT be used in any manner, + including advertising or publicity pertaining to the Advanced Messaging + Queue Protocol Specification or its contents without specific, written + prior permission. Title to copyright in the Advanced Messaging Queue + Protocol Specification will at all times remain with the Authors. + + No other rights are granted by implication, estoppel or otherwise. + + Upon termination of your license or rights under this Agreement, you + shall destroy all copies of the Advanced Messaging Queue Protocol + Specification in your possession or control. + + Trademarks + ========== + JPMorgan, JPMorgan Chase, Chase, the JPMorgan Chase logo and the + Octagon Symbol are trademarks of JPMorgan Chase & Co. + + IMATIX and the iMatix logo are trademarks of iMatix Corporation sprl. + + IONA, IONA Technologies, and the IONA logos are trademarks of IONA + Technologies PLC and/or its subsidiaries. + + LINUX is a trademark of Linus Torvalds. RED HAT and JBOSS are registered + trademarks of Red Hat, Inc. in the US and other countries. + + Java, all Java-based trademarks and OpenOffice.org are trademarks of + Sun Microsystems, Inc. in the United States, other countries, or both. + + Other company, product, or service names may be trademarks or service + marks of others. + + Links to full AMQP specification: + ================================= + http://www.amqp.org +--> + +<!-- + <!DOCTYPE amqp SYSTEM "amqp.dtd"> +--> + +<!-- XML Notes + + We use entities to indicate repetition; attributes to indicate properties. + + We use the 'name' attribute as an identifier, usually within the context + of the surrounding entities. + + We use spaces to seperate words in names, so that we can print names in + their natural form depending on the context - underlines for source code, + hyphens for written text, etc. + + We do not enforce any particular validation mechanism but we support all + mechanisms. The protocol definition conforms to a formal grammar that is + published seperately in several technologies. + + --> + +<amqp major = "0" minor = "9" revision = "1" + port = "5672" comment = "AMQ Protocol version 0-9-1"> + <!-- + ====================================================== + == CONSTANTS + ====================================================== + --> + <!-- Frame types --> + <constant name = "frame-method" value = "1" /> + <constant name = "frame-header" value = "2" /> + <constant name = "frame-body" value = "3" /> + <constant name = "frame-heartbeat" value = "8" /> + + <!-- Protocol constants --> + <constant name = "frame-min-size" value = "4096" /> + <constant name = "frame-end" value = "206" /> + + <!-- Reply codes --> + <constant name = "reply-success" value = "200"> + <doc> + Indicates that the method completed successfully. This reply code is + reserved for future use - the current protocol design does not use positive + confirmation and reply codes are sent only in case of an error. + </doc> + </constant> + + <constant name = "content-too-large" value = "311" class = "soft-error"> + <doc> + The client attempted to transfer content larger than the server could accept + at the present time. The client may retry at a later time. + </doc> + </constant> + + <constant name = "no-consumers" value = "313" class = "soft-error"> + <doc> + When the exchange cannot deliver to a consumer when the immediate flag is + set. As a result of pending data on the queue or the absence of any + consumers of the queue. + </doc> + </constant> + + <constant name = "connection-forced" value = "320" class = "hard-error"> + <doc> + An operator intervened to close the connection for some reason. The client + may retry at some later date. + </doc> + </constant> + + <constant name = "invalid-path" value = "402" class = "hard-error"> + <doc> + The client tried to work with an unknown virtual host. + </doc> + </constant> + + <constant name = "access-refused" value = "403" class = "soft-error"> + <doc> + The client attempted to work with a server entity to which it has no + access due to security settings. + </doc> + </constant> + + <constant name = "not-found" value = "404" class = "soft-error"> + <doc> + The client attempted to work with a server entity that does not exist. + </doc> + </constant> + + <constant name = "resource-locked" value = "405" class = "soft-error"> + <doc> + The client attempted to work with a server entity to which it has no + access because another client is working with it. + </doc> + </constant> + + <constant name = "precondition-failed" value = "406" class = "soft-error"> + <doc> + The client requested a method that was not allowed because some precondition + failed. + </doc> + </constant> + + <constant name = "frame-error" value = "501" class = "hard-error"> + <doc> + The sender sent a malformed frame that the recipient could not decode. + This strongly implies a programming error in the sending peer. + </doc> + </constant> + + <constant name = "syntax-error" value = "502" class = "hard-error"> + <doc> + The sender sent a frame that contained illegal values for one or more + fields. This strongly implies a programming error in the sending peer. + </doc> + </constant> + + <constant name = "command-invalid" value = "503" class = "hard-error"> + <doc> + The client sent an invalid sequence of frames, attempting to perform an + operation that was considered invalid by the server. This usually implies + a programming error in the client. + </doc> + </constant> + + <constant name = "channel-error" value = "504" class = "hard-error"> + <doc> + The client attempted to work with a channel that had not been correctly + opened. This most likely indicates a fault in the client layer. + </doc> + </constant> + + <constant name = "unexpected-frame" value = "505" class = "hard-error"> + <doc> + The peer sent a frame that was not expected, usually in the context of + a content header and body. This strongly indicates a fault in the peer's + content processing. + </doc> + </constant> + + <constant name = "resource-error" value = "506" class = "hard-error"> + <doc> + The server could not complete the method because it lacked sufficient + resources. This may be due to the client creating too many of some type + of entity. + </doc> + </constant> + + <constant name = "not-allowed" value = "530" class = "hard-error"> + <doc> + The client tried to work with some entity in a manner that is prohibited + by the server, due to security settings or by some other criteria. + </doc> + </constant> + + <constant name = "not-implemented" value = "540" class = "hard-error"> + <doc> + The client tried to use functionality that is not implemented in the + server. + </doc> + </constant> + + <constant name = "internal-error" value = "541" class = "hard-error"> + <doc> + The server could not complete the method because of an internal error. + The server may require intervention by an operator in order to resume + normal operations. + </doc> + </constant> + + <!-- + ====================================================== + == DOMAIN TYPES + ====================================================== + --> + + <domain name = "class-id" type = "short" /> + + <domain name = "consumer-tag" type = "shortstr" label = "consumer tag"> + <doc> + Identifier for the consumer, valid within the current channel. + </doc> + </domain> + + <domain name = "delivery-tag" type = "longlong" label = "server-assigned delivery tag"> + <doc> + The server-assigned and channel-specific delivery tag + </doc> + <rule name = "channel-local"> + <doc> + The delivery tag is valid only within the channel from which the message was + received. I.e. a client MUST NOT receive a message on one channel and then + acknowledge it on another. + </doc> + </rule> + <rule name = "non-zero"> + <doc> + The server MUST NOT use a zero value for delivery tags. Zero is reserved + for client use, meaning "all messages so far received". + </doc> + </rule> + </domain> + + <domain name = "exchange-name" type = "shortstr" label = "exchange name"> + <doc> + The exchange name is a client-selected string that identifies the exchange for + publish methods. + </doc> + <assert check = "length" value = "127" /> + <assert check = "regexp" value = "^[a-zA-Z0-9-_.:]*$" /> + </domain> + + <domain name = "method-id" type = "short" /> + + <domain name = "no-ack" type = "bit" label = "no acknowledgement needed"> + <doc> + If this field is set the server does not expect acknowledgements for + messages. That is, when a message is delivered to the client the server + assumes the delivery will succeed and immediately dequeues it. This + functionality may increase performance but at the cost of reliability. + Messages can get lost if a client dies before they are delivered to the + application. + </doc> + </domain> + + <domain name = "no-local" type = "bit" label = "do not deliver own messages"> + <doc> + If the no-local field is set the server will not send messages to the connection that + published them. + </doc> + </domain> + + <domain name = "no-wait" type = "bit" label = "do not send reply method"> + <doc> + If set, the server will not respond to the method. The client should not wait + for a reply method. If the server could not complete the method it will raise a + channel or connection exception. + </doc> + </domain> + + <domain name = "path" type = "shortstr"> + <doc> + Unconstrained. + </doc> + <assert check = "notnull" /> + <assert check = "length" value = "127" /> + </domain> + + <domain name = "peer-properties" type = "table"> + <doc> + This table provides a set of peer properties, used for identification, debugging, + and general information. + </doc> + </domain> + + <domain name = "queue-name" type = "shortstr" label = "queue name"> + <doc> + The queue name identifies the queue within the vhost. In methods where the queue + name may be blank, and that has no specific significance, this refers to the + 'current' queue for the channel, meaning the last queue that the client declared + on the channel. If the client did not declare a queue, and the method needs a + queue name, this will result in a 502 (syntax error) channel exception. + </doc> + <assert check = "length" value = "127" /> + <assert check = "regexp" value = "^[a-zA-Z0-9-_.:]*$" /> + </domain> + + <domain name = "redelivered" type = "bit" label = "message is being redelivered"> + <doc> + This indicates that the message has been previously delivered to this or + another client. + </doc> + <rule name = "implementation"> + <doc> + The server SHOULD try to signal redelivered messages when it can. When + redelivering a message that was not successfully acknowledged, the server + SHOULD deliver it to the original client if possible. + </doc> + <doc type = "scenario"> + Declare a shared queue and publish a message to the queue. Consume the + message using explicit acknowledgements, but do not acknowledge the + message. Close the connection, reconnect, and consume from the queue + again. The message should arrive with the redelivered flag set. + </doc> + </rule> + <rule name = "hinting"> + <doc> + The client MUST NOT rely on the redelivered field but should take it as a + hint that the message may already have been processed. A fully robust + client must be able to track duplicate received messages on non-transacted, + and locally-transacted channels. + </doc> + </rule> + </domain> + + <domain name = "message-count" type = "long" label = "number of messages in queue"> + <doc> + The number of messages in the queue, which will be zero for newly-declared + queues. This is the number of messages present in the queue, and committed + if the channel on which they were published is transacted, that are not + waiting acknowledgement. + </doc> + </domain> + + <domain name = "reply-code" type = "short" label = "reply code from server"> + <doc> + The reply code. The AMQ reply codes are defined as constants at the start + of this formal specification. + </doc> + <assert check = "notnull" /> + </domain> + + <domain name = "reply-text" type = "shortstr" label = "localised reply text"> + <doc> + The localised reply text. This text can be logged as an aid to resolving + issues. + </doc> + <assert check = "notnull" /> + </domain> + + <!-- Elementary domains --> + <domain name = "bit" type = "bit" label = "single bit" /> + <domain name = "octet" type = "octet" label = "single octet" /> + <domain name = "short" type = "short" label = "16-bit integer" /> + <domain name = "long" type = "long" label = "32-bit integer" /> + <domain name = "longlong" type = "longlong" label = "64-bit integer" /> + <domain name = "shortstr" type = "shortstr" label = "short string" /> + <domain name = "longstr" type = "longstr" label = "long string" /> + <domain name = "timestamp" type = "timestamp" label = "64-bit timestamp" /> + <domain name = "table" type = "table" label = "field table" /> + + <!-- == CONNECTION ======================================================= --> + + <class name = "connection" handler = "connection" index = "10" label = "work with socket connections"> + <doc> + The connection class provides methods for a client to establish a network connection to + a server, and for both peers to operate the connection thereafter. + </doc> + + <doc type = "grammar"> + connection = open-connection *use-connection close-connection + open-connection = C:protocol-header + S:START C:START-OK + *challenge + S:TUNE C:TUNE-OK + C:OPEN S:OPEN-OK + challenge = S:SECURE C:SECURE-OK + use-connection = *channel + close-connection = C:CLOSE S:CLOSE-OK + / S:CLOSE C:CLOSE-OK + </doc> + + <chassis name = "server" implement = "MUST" /> + <chassis name = "client" implement = "MUST" /> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "start" synchronous = "1" index = "10" label = "start connection negotiation"> + <doc> + This method starts the connection negotiation process by telling the client the + protocol version that the server proposes, along with a list of security mechanisms + which the client can use for authentication. + </doc> + + <rule name = "protocol-name"> + <doc> + If the server cannot support the protocol specified in the protocol header, + it MUST respond with a valid protocol header and then close the socket + connection. + </doc> + <doc type = "scenario"> + The client sends a protocol header containing an invalid protocol name. + The server MUST respond by sending a valid protocol header and then closing + the connection. + </doc> + </rule> + <rule name = "server-support"> + <doc> + The server MUST provide a protocol version that is lower than or equal to + that requested by the client in the protocol header. + </doc> + <doc type = "scenario"> + The client requests a protocol version that is higher than any valid + implementation, e.g. 2.0. The server must respond with a protocol header + indicating its supported protocol version, e.g. 1.0. + </doc> + </rule> + <rule name = "client-support"> + <doc> + If the client cannot handle the protocol version suggested by the server + it MUST close the socket connection without sending any further data. + </doc> + <doc type = "scenario"> + The server sends a protocol version that is lower than any valid + implementation, e.g. 0.1. The client must respond by closing the + connection without sending any further data. + </doc> + </rule> + + <chassis name = "client" implement = "MUST" /> + <response name = "start-ok" /> + + <field name = "version-major" domain = "octet" label = "protocol major version"> + <doc> + The major version number can take any value from 0 to 99 as defined in the + AMQP specification. + </doc> + </field> + + <field name = "version-minor" domain = "octet" label = "protocol minor version"> + <doc> + The minor version number can take any value from 0 to 99 as defined in the + AMQP specification. + </doc> + </field> + + <field name = "server-properties" domain = "peer-properties" label = "server properties"> + <rule name = "required-fields"> + <doc> + The properties SHOULD contain at least these fields: "host", specifying the + server host name or address, "product", giving the name of the server product, + "version", giving the name of the server version, "platform", giving the name + of the operating system, "copyright", if appropriate, and "information", giving + other general information. + </doc> + <doc type = "scenario"> + Client connects to server and inspects the server properties. It checks for + the presence of the required fields. + </doc> + </rule> + </field> + + <field name = "mechanisms" domain = "longstr" label = "available security mechanisms"> + <doc> + A list of the security mechanisms that the server supports, delimited by spaces. + </doc> + <assert check = "notnull" /> + </field> + + <field name = "locales" domain = "longstr" label = "available message locales"> + <doc> + A list of the message locales that the server supports, delimited by spaces. The + locale defines the language in which the server will send reply texts. + </doc> + <rule name = "required-support"> + <doc> + The server MUST support at least the en_US locale. + </doc> + <doc type = "scenario"> + Client connects to server and inspects the locales field. It checks for + the presence of the required locale(s). + </doc> + </rule> + <assert check = "notnull" /> + </field> + </method> + + <method name = "start-ok" synchronous = "1" index = "11" + label = "select security mechanism and locale"> + <doc> + This method selects a SASL security mechanism. + </doc> + + <chassis name = "server" implement = "MUST" /> + + <field name = "client-properties" domain = "peer-properties" label = "client properties"> + <rule name = "required-fields"> + <!-- This rule is not testable from the client side --> + <doc> + The properties SHOULD contain at least these fields: "product", giving the name + of the client product, "version", giving the name of the client version, "platform", + giving the name of the operating system, "copyright", if appropriate, and + "information", giving other general information. + </doc> + </rule> + </field> + + <field name = "mechanism" domain = "shortstr" label = "selected security mechanism"> + <doc> + A single security mechanisms selected by the client, which must be one of those + specified by the server. + </doc> + <rule name = "security"> + <doc> + The client SHOULD authenticate using the highest-level security profile it + can handle from the list provided by the server. + </doc> + </rule> + <rule name = "validity"> + <doc> + If the mechanism field does not contain one of the security mechanisms + proposed by the server in the Start method, the server MUST close the + connection without sending any further data. + </doc> + <doc type = "scenario"> + Client connects to server and sends an invalid security mechanism. The + server must respond by closing the connection (a socket close, with no + connection close negotiation). + </doc> + </rule> + <assert check = "notnull" /> + </field> + + <field name = "response" domain = "longstr" label = "security response data"> + <doc> + A block of opaque data passed to the security mechanism. The contents of this + data are defined by the SASL security mechanism. + </doc> + <assert check = "notnull" /> + </field> + + <field name = "locale" domain = "shortstr" label = "selected message locale"> + <doc> + A single message locale selected by the client, which must be one of those + specified by the server. + </doc> + <assert check = "notnull" /> + </field> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "secure" synchronous = "1" index = "20" label = "security mechanism challenge"> + <doc> + The SASL protocol works by exchanging challenges and responses until both peers have + received sufficient information to authenticate each other. This method challenges + the client to provide more information. + </doc> + + <chassis name = "client" implement = "MUST" /> + <response name = "secure-ok" /> + + <field name = "challenge" domain = "longstr" label = "security challenge data"> + <doc> + Challenge information, a block of opaque binary data passed to the security + mechanism. + </doc> + </field> + </method> + + <method name = "secure-ok" synchronous = "1" index = "21" label = "security mechanism response"> + <doc> + This method attempts to authenticate, passing a block of SASL data for the security + mechanism at the server side. + </doc> + + <chassis name = "server" implement = "MUST" /> + + <field name = "response" domain = "longstr" label = "security response data"> + <doc> + A block of opaque data passed to the security mechanism. The contents of this + data are defined by the SASL security mechanism. + </doc> + <assert check = "notnull" /> + </field> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "tune" synchronous = "1" index = "30" + label = "propose connection tuning parameters"> + <doc> + This method proposes a set of connection configuration values to the client. The + client can accept and/or adjust these. + </doc> + + <chassis name = "client" implement = "MUST" /> + + <response name = "tune-ok" /> + + <field name = "channel-max" domain = "short" label = "proposed maximum channels"> + <doc> + Specifies highest channel number that the server permits. Usable channel numbers + are in the range 1..channel-max. Zero indicates no specified limit. + </doc> + </field> + + <field name = "frame-max" domain = "long" label = "proposed maximum frame size"> + <doc> + The largest frame size that the server proposes for the connection, including + frame header and end-byte. The client can negotiate a lower value. Zero means + that the server does not impose any specific limit but may reject very large + frames if it cannot allocate resources for them. + </doc> + <rule name = "minimum"> + <doc> + Until the frame-max has been negotiated, both peers MUST accept frames of up + to frame-min-size octets large, and the minimum negotiated value for frame-max + is also frame-min-size. + </doc> + <doc type = "scenario"> + Client connects to server and sends a large properties field, creating a frame + of frame-min-size octets. The server must accept this frame. + </doc> + </rule> + </field> + + <field name = "heartbeat" domain = "short" label = "desired heartbeat delay"> + <doc> + The delay, in seconds, of the connection heartbeat that the server wants. + Zero means the server does not want a heartbeat. + </doc> + </field> + </method> + + <method name = "tune-ok" synchronous = "1" index = "31" + label = "negotiate connection tuning parameters"> + <doc> + This method sends the client's connection tuning parameters to the server. + Certain fields are negotiated, others provide capability information. + </doc> + + <chassis name = "server" implement = "MUST" /> + + <field name = "channel-max" domain = "short" label = "negotiated maximum channels"> + <doc> + The maximum total number of channels that the client will use per connection. + </doc> + <rule name = "upper-limit"> + <doc> + If the client specifies a channel max that is higher than the value provided + by the server, the server MUST close the connection without attempting a + negotiated close. The server may report the error in some fashion to assist + implementors. + </doc> + </rule> + <assert check = "notnull" /> + <assert check = "le" method = "tune" field = "channel-max" /> + </field> + + <field name = "frame-max" domain = "long" label = "negotiated maximum frame size"> + <doc> + The largest frame size that the client and server will use for the connection. + Zero means that the client does not impose any specific limit but may reject + very large frames if it cannot allocate resources for them. Note that the + frame-max limit applies principally to content frames, where large contents can + be broken into frames of arbitrary size. + </doc> + <rule name = "minimum"> + <doc> + Until the frame-max has been negotiated, both peers MUST accept frames of up + to frame-min-size octets large, and the minimum negotiated value for frame-max + is also frame-min-size. + </doc> + </rule> + <rule name = "upper-limit"> + <doc> + If the client specifies a frame max that is higher than the value provided + by the server, the server MUST close the connection without attempting a + negotiated close. The server may report the error in some fashion to assist + implementors. + </doc> + </rule> + </field> + + <field name = "heartbeat" domain = "short" label = "desired heartbeat delay"> + <doc> + The delay, in seconds, of the connection heartbeat that the client wants. Zero + means the client does not want a heartbeat. + </doc> + </field> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "open" synchronous = "1" index = "40" label = "open connection to virtual host"> + <doc> + This method opens a connection to a virtual host, which is a collection of + resources, and acts to separate multiple application domains within a server. + The server may apply arbitrary limits per virtual host, such as the number + of each type of entity that may be used, per connection and/or in total. + </doc> + + <chassis name = "server" implement = "MUST" /> + <response name = "open-ok" /> + + <field name = "virtual-host" domain = "path" label = "virtual host name"> + <doc> + The name of the virtual host to work with. + </doc> + <rule name = "separation"> + <doc> + If the server supports multiple virtual hosts, it MUST enforce a full + separation of exchanges, queues, and all associated entities per virtual + host. An application, connected to a specific virtual host, MUST NOT be able + to access resources of another virtual host. + </doc> + </rule> + <rule name = "security"> + <doc> + The server SHOULD verify that the client has permission to access the + specified virtual host. + </doc> + </rule> + </field> + <!-- Deprecated: "capabilities", must be zero --> + <field name = "reserved-1" type = "shortstr" reserved = "1" /> + <!-- Deprecated: "insist", must be zero --> + <field name = "reserved-2" type = "bit" reserved = "1" /> + </method> + + <method name = "open-ok" synchronous = "1" index = "41" label = "signal that connection is ready"> + <doc> + This method signals to the client that the connection is ready for use. + </doc> + <chassis name = "client" implement = "MUST" /> + <!-- Deprecated: "known-hosts", must be zero --> + <field name = "reserved-1" type = "shortstr" reserved = "1" /> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "close" synchronous = "1" index = "50" label = "request a connection close"> + <doc> + This method indicates that the sender wants to close the connection. This may be + due to internal conditions (e.g. a forced shut-down) or due to an error handling + a specific method, i.e. an exception. When a close is due to an exception, the + sender provides the class and method id of the method which caused the exception. + </doc> + <rule name = "stability"> + <doc> + After sending this method, any received methods except Close and Close-OK MUST + be discarded. The response to receiving a Close after sending Close must be to + send Close-Ok. + </doc> + </rule> + + <chassis name = "client" implement = "MUST" /> + <chassis name = "server" implement = "MUST" /> + <response name = "close-ok" /> + + <field name = "reply-code" domain = "reply-code" /> + <field name = "reply-text" domain = "reply-text" /> + + <field name = "class-id" domain = "class-id" label = "failing method class"> + <doc> + When the close is provoked by a method exception, this is the class of the + method. + </doc> + </field> + + <field name = "method-id" domain = "method-id" label = "failing method ID"> + <doc> + When the close is provoked by a method exception, this is the ID of the method. + </doc> + </field> + </method> + + <method name = "close-ok" synchronous = "1" index = "51" label = "confirm a connection close"> + <doc> + This method confirms a Connection.Close method and tells the recipient that it is + safe to release resources for the connection and close the socket. + </doc> + <rule name = "reporting"> + <doc> + A peer that detects a socket closure without having received a Close-Ok + handshake method SHOULD log the error. + </doc> + </rule> + <chassis name = "client" implement = "MUST" /> + <chassis name = "server" implement = "MUST" /> + </method> + + <method name = "blocked" index = "60"> + <doc> + This method indicates that a connection has been blocked + and does not accept new publishes. + </doc> + <chassis name = "server" implement = "MUST"/> + <chassis name = "client" implement = "MUST"/> + <field name = "reason" domain = "shortstr" /> + </method> + <method name = "unblocked" index = "61"> + <doc> + This method indicates that a connection has been unblocked + and now accepts publishes. + </doc> + <chassis name = "server" implement = "MUST"/> + <chassis name = "client" implement = "MUST"/> + </method> + </class> + + <!-- == CHANNEL ========================================================== --> + + <class name = "channel" handler = "channel" index = "20" label = "work with channels"> + <doc> + The channel class provides methods for a client to establish a channel to a + server and for both peers to operate the channel thereafter. + </doc> + + <doc type = "grammar"> + channel = open-channel *use-channel close-channel + open-channel = C:OPEN S:OPEN-OK + use-channel = C:FLOW S:FLOW-OK + / S:FLOW C:FLOW-OK + / functional-class + close-channel = C:CLOSE S:CLOSE-OK + / S:CLOSE C:CLOSE-OK + </doc> + + <chassis name = "server" implement = "MUST" /> + <chassis name = "client" implement = "MUST" /> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "open" synchronous = "1" index = "10" label = "open a channel for use"> + <doc> + This method opens a channel to the server. + </doc> + <rule name = "state" on-failure = "channel-error"> + <doc> + The client MUST NOT use this method on an already-opened channel. + </doc> + <doc type = "scenario"> + Client opens a channel and then reopens the same channel. + </doc> + </rule> + <chassis name = "server" implement = "MUST" /> + <response name = "open-ok" /> + <!-- Deprecated: "out-of-band", must be zero --> + <field name = "reserved-1" type = "shortstr" reserved = "1" /> + </method> + + <method name = "open-ok" synchronous = "1" index = "11" label = "signal that the channel is ready"> + <doc> + This method signals to the client that the channel is ready for use. + </doc> + <chassis name = "client" implement = "MUST" /> + <!-- Deprecated: "channel-id", must be zero --> + <field name = "reserved-1" type = "longstr" reserved = "1" /> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "flow" synchronous = "1" index = "20" label = "enable/disable flow from peer"> + <doc> + This method asks the peer to pause or restart the flow of content data sent by + a consumer. This is a simple flow-control mechanism that a peer can use to avoid + overflowing its queues or otherwise finding itself receiving more messages than + it can process. Note that this method is not intended for window control. It does + not affect contents returned by Basic.Get-Ok methods. + </doc> + + <rule name = "initial-state"> + <doc> + When a new channel is opened, it is active (flow is active). Some applications + assume that channels are inactive until started. To emulate this behaviour a + client MAY open the channel, then pause it. + </doc> + </rule> + + <rule name = "bidirectional"> + <doc> + When sending content frames, a peer SHOULD monitor the channel for incoming + methods and respond to a Channel.Flow as rapidly as possible. + </doc> + </rule> + + <rule name = "throttling"> + <doc> + A peer MAY use the Channel.Flow method to throttle incoming content data for + internal reasons, for example, when exchanging data over a slower connection. + </doc> + </rule> + + <rule name = "expected-behaviour"> + <doc> + The peer that requests a Channel.Flow method MAY disconnect and/or ban a peer + that does not respect the request. This is to prevent badly-behaved clients + from overwhelming a server. + </doc> + </rule> + + <chassis name = "server" implement = "MUST" /> + <chassis name = "client" implement = "MUST" /> + + <response name = "flow-ok" /> + + <field name = "active" domain = "bit" label = "start/stop content frames"> + <doc> + If 1, the peer starts sending content frames. If 0, the peer stops sending + content frames. + </doc> + </field> + </method> + + <method name = "flow-ok" index = "21" label = "confirm a flow method"> + <doc> + Confirms to the peer that a flow command was received and processed. + </doc> + <chassis name = "server" implement = "MUST" /> + <chassis name = "client" implement = "MUST" /> + <field name = "active" domain = "bit" label = "current flow setting"> + <doc> + Confirms the setting of the processed flow method: 1 means the peer will start + sending or continue to send content frames; 0 means it will not. + </doc> + </field> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "close" synchronous = "1" index = "40" label = "request a channel close"> + <doc> + This method indicates that the sender wants to close the channel. This may be due to + internal conditions (e.g. a forced shut-down) or due to an error handling a specific + method, i.e. an exception. When a close is due to an exception, the sender provides + the class and method id of the method which caused the exception. + </doc> + <rule name = "stability"> + <doc> + After sending this method, any received methods except Close and Close-OK MUST + be discarded. The response to receiving a Close after sending Close must be to + send Close-Ok. + </doc> + </rule> + + <chassis name = "client" implement = "MUST" /> + <chassis name = "server" implement = "MUST" /> + <response name = "close-ok" /> + + <field name = "reply-code" domain = "reply-code" /> + <field name = "reply-text" domain = "reply-text" /> + + <field name = "class-id" domain = "class-id" label = "failing method class"> + <doc> + When the close is provoked by a method exception, this is the class of the + method. + </doc> + </field> + + <field name = "method-id" domain = "method-id" label = "failing method ID"> + <doc> + When the close is provoked by a method exception, this is the ID of the method. + </doc> + </field> + </method> + + <method name = "close-ok" synchronous = "1" index = "41" label = "confirm a channel close"> + <doc> + This method confirms a Channel.Close method and tells the recipient that it is safe + to release resources for the channel. + </doc> + <rule name = "reporting"> + <doc> + A peer that detects a socket closure without having received a Channel.Close-Ok + handshake method SHOULD log the error. + </doc> + </rule> + <chassis name = "client" implement = "MUST" /> + <chassis name = "server" implement = "MUST" /> + </method> + </class> + + <!-- == EXCHANGE ========================================================= --> + + <class name = "exchange" handler = "channel" index = "40" label = "work with exchanges"> + <doc> + Exchanges match and distribute messages across queues. Exchanges can be configured in + the server or declared at runtime. + </doc> + + <doc type = "grammar"> + exchange = C:DECLARE S:DECLARE-OK + / C:DELETE S:DELETE-OK + / C:BIND S:BIND-OK + / C:UNBIND S:UNBIND-OK + </doc> + + <chassis name = "server" implement = "MUST" /> + <chassis name = "client" implement = "MUST" /> + + <rule name = "required-types"> + <doc> + The server MUST implement these standard exchange types: fanout, direct. + </doc> + <doc type = "scenario"> + Client attempts to declare an exchange with each of these standard types. + </doc> + </rule> + <rule name = "recommended-types"> + <doc> + The server SHOULD implement these standard exchange types: topic, headers. + </doc> + <doc type = "scenario"> + Client attempts to declare an exchange with each of these standard types. + </doc> + </rule> + <rule name = "required-instances"> + <doc> + The server MUST, in each virtual host, pre-declare an exchange instance + for each standard exchange type that it implements, where the name of the + exchange instance, if defined, is "amq." followed by the exchange type name. + </doc> + <doc> + The server MUST, in each virtual host, pre-declare at least two direct + exchange instances: one named "amq.direct", the other with no public name + that serves as a default exchange for Publish methods. + </doc> + <doc type = "scenario"> + Client declares a temporary queue and attempts to bind to each required + exchange instance ("amq.fanout", "amq.direct", "amq.topic", and "amq.headers" + if those types are defined). + </doc> + </rule> + <rule name = "default-exchange"> + <doc> + The server MUST pre-declare a direct exchange with no public name to act as + the default exchange for content Publish methods and for default queue bindings. + </doc> + <doc type = "scenario"> + Client checks that the default exchange is active by specifying a queue + binding with no exchange name, and publishing a message with a suitable + routing key but without specifying the exchange name, then ensuring that + the message arrives in the queue correctly. + </doc> + </rule> + <rule name = "default-access"> + <doc> + The server MUST NOT allow clients to access the default exchange except + by specifying an empty exchange name in the Queue.Bind and content Publish + methods. + </doc> + </rule> + <rule name = "extensions"> + <doc> + The server MAY implement other exchange types as wanted. + </doc> + </rule> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "declare" synchronous = "1" index = "10" label = "verify exchange exists, create if needed"> + <doc> + This method creates an exchange if it does not already exist, and if the exchange + exists, verifies that it is of the correct and expected class. + </doc> + <rule name = "minimum"> + <doc> + The server SHOULD support a minimum of 16 exchanges per virtual host and + ideally, impose no limit except as defined by available resources. + </doc> + <doc type = "scenario"> + The client declares as many exchanges as it can until the server reports + an error; the number of exchanges successfully declared must be at least + sixteen. + </doc> + </rule> + + <chassis name = "server" implement = "MUST" /> + <response name = "declare-ok" /> + + <!-- Deprecated: "ticket", must be zero --> + <field name = "reserved-1" type = "short" reserved = "1" /> + + <field name = "exchange" domain = "exchange-name"> + <rule name = "reserved" on-failure = "access-refused"> + <doc> + Exchange names starting with "amq." are reserved for pre-declared and + standardised exchanges. The client MAY declare an exchange starting with + "amq." if the passive option is set, or the exchange already exists. + </doc> + <doc type = "scenario"> + The client attempts to declare a non-existing exchange starting with + "amq." and with the passive option set to zero. + </doc> + </rule> + <rule name = "syntax" on-failure = "precondition-failed"> + <doc> + The exchange name consists of a non-empty sequence of these characters: + letters, digits, hyphen, underscore, period, or colon. + </doc> + <doc type = "scenario"> + The client attempts to declare an exchange with an illegal name. + </doc> + </rule> + <assert check = "notnull" /> + </field> + + <field name = "type" domain = "shortstr" label = "exchange type"> + <doc> + Each exchange belongs to one of a set of exchange types implemented by the + server. The exchange types define the functionality of the exchange - i.e. how + messages are routed through it. It is not valid or meaningful to attempt to + change the type of an existing exchange. + </doc> + <rule name = "typed" on-failure = "not-allowed"> + <doc> + Exchanges cannot be redeclared with different types. The client MUST not + attempt to redeclare an existing exchange with a different type than used + in the original Exchange.Declare method. + </doc> + <doc type = "scenario"> + TODO. + </doc> + </rule> + <rule name = "support" on-failure = "command-invalid"> + <doc> + The client MUST NOT attempt to declare an exchange with a type that the + server does not support. + </doc> + <doc type = "scenario"> + TODO. + </doc> + </rule> + </field> + + <field name = "passive" domain = "bit" label = "do not create exchange"> + <doc> + If set, the server will reply with Declare-Ok if the exchange already + exists with the same name, and raise an error if not. The client can + use this to check whether an exchange exists without modifying the + server state. When set, all other method fields except name and no-wait + are ignored. A declare with both passive and no-wait has no effect. + Arguments are compared for semantic equivalence. + </doc> + <rule name = "not-found"> + <doc> + If set, and the exchange does not already exist, the server MUST + raise a channel exception with reply code 404 (not found). + </doc> + <doc type = "scenario"> + TODO. + </doc> + </rule> + <rule name = "equivalent"> + <doc> + If not set and the exchange exists, the server MUST check that the + existing exchange has the same values for type, durable, and arguments + fields. The server MUST respond with Declare-Ok if the requested + exchange matches these fields, and MUST raise a channel exception if + not. + </doc> + <doc type = "scenario"> + TODO. + </doc> + </rule> + </field> + + <field name = "durable" domain = "bit" label = "request a durable exchange"> + <doc> + If set when creating a new exchange, the exchange will be marked as durable. + Durable exchanges remain active when a server restarts. Non-durable exchanges + (transient exchanges) are purged if/when a server restarts. + </doc> + <rule name = "support"> + <doc> + The server MUST support both durable and transient exchanges. + </doc> + <doc type = "scenario"> + TODO. + </doc> + </rule> + </field> + + <field name = "auto-delete" domain = "bit" label = "auto-delete when unused"> + <doc> + If set, the exchange is deleted when all queues have + finished using it. + </doc> + <rule name = "amq_exchange_02"> + <doc> + The server SHOULD allow for a reasonable delay between the + point when it determines that an exchange is not being + used (or no longer used), and the point when it deletes + the exchange. At the least it must allow a client to + create an exchange and then bind a queue to it, with a + small but non-zero delay between these two actions. + </doc> + </rule> + <rule name = "amq_exchange_25"> + <doc> + The server MUST ignore the auto-delete field if the + exchange already exists. + </doc> + </rule> + </field> + + <field name = "internal" domain = "bit" label = "create internal exchange"> + <doc> + If set, the exchange may not be used directly by publishers, + but only when bound to other exchanges. Internal exchanges + are used to construct wiring that is not visible to + applications. + </doc> + </field> + + <field name = "no-wait" domain = "no-wait" /> + + <field name = "arguments" domain = "table" label = "arguments for declaration"> + <doc> + A set of arguments for the declaration. The syntax and semantics of these + arguments depends on the server implementation. + </doc> + </field> + </method> + + <method name = "declare-ok" synchronous = "1" index = "11" label = "confirm exchange declaration"> + <doc> + This method confirms a Declare method and confirms the name of the exchange, + essential for automatically-named exchanges. + </doc> + <chassis name = "client" implement = "MUST" /> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "delete" synchronous = "1" index = "20" label = "delete an exchange"> + <doc> + This method deletes an exchange. When an exchange is deleted all queue bindings on + the exchange are cancelled. + </doc> + + <chassis name = "server" implement = "MUST" /> + <response name = "delete-ok" /> + + <!-- Deprecated: "ticket", must be zero --> + <field name = "reserved-1" type = "short" reserved = "1" /> + + <field name = "exchange" domain = "exchange-name"> + <rule name = "exists" on-failure = "not-found"> + <doc> + The client MUST NOT attempt to delete an exchange that does not exist. + </doc> + </rule> + <assert check = "notnull" /> + </field> + + <field name = "if-unused" domain = "bit" label = "delete only if unused"> + <doc> + If set, the server will only delete the exchange if it has no queue bindings. If + the exchange has queue bindings the server does not delete it but raises a + channel exception instead. + </doc> + <rule name = "in-use" on-failure = "precondition-failed"> + <doc> + The server MUST NOT delete an exchange that has bindings on it, if the if-unused + field is true. + </doc> + <doc type = "scenario"> + The client declares an exchange, binds a queue to it, then tries to delete it + setting if-unused to true. + </doc> + </rule> + </field> + + <field name = "no-wait" domain = "no-wait" /> + </method> + + <method name = "delete-ok" synchronous = "1" index = "21" + label = "confirm deletion of an exchange"> + <doc>This method confirms the deletion of an exchange.</doc> + <chassis name = "client" implement = "MUST" /> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "bind" synchronous = "1" index = "30" + label = "bind exchange to an exchange"> + + <doc>This method binds an exchange to an exchange.</doc> + + <rule name = "duplicates"> + <doc> + A server MUST allow and ignore duplicate bindings - that is, + two or more bind methods for a specific exchanges, with + identical arguments - without treating these as an error. + </doc> + <doc type = "scenario"> + A client binds an exchange to an exchange. The client then + repeats the bind (with identical arguments). + </doc> + </rule> + + <rule name = "cyclical"> + <doc> + A server MUST allow cycles of exchange bindings to be + created including allowing an exchange to be bound to + itself. + </doc> + <doc type = "scenario"> + A client declares an exchange and binds it to itself. + </doc> + </rule> + + <rule name = "unique"> + <doc> + A server MUST not deliver the same message more than once to + a destination exchange, even if the topology of exchanges + and bindings results in multiple (even infinite) routes to + that exchange. + </doc> + <doc type = "scenario"> + A client declares an exchange and binds it using multiple + bindings to the amq.topic exchange. The client then + publishes a message to the amq.topic exchange that matches + all the bindings. + </doc> + </rule> + + <chassis name = "server" implement = "MUST"/> + + <response name = "bind-ok"/> + + <!-- Deprecated: "ticket", must be zero --> + <field name = "reserved-1" type = "short" reserved = "1"/> + + <field name = "destination" domain = "exchange-name" + label = "name of the destination exchange to bind to"> + <doc>Specifies the name of the destination exchange to bind.</doc> + <rule name = "exchange-existence" on-failure = "not-found"> + <doc> + A client MUST NOT be allowed to bind a non-existent + destination exchange. + </doc> + <doc type = "scenario"> + A client attempts to bind an undeclared exchange to an + exchange. + </doc> + </rule> + <rule name = "default-exchange"> + <doc> + The server MUST accept a blank exchange name to mean the + default exchange. + </doc> + <doc type = "scenario"> + The client declares an exchange and binds a blank exchange + name to it. + </doc> + </rule> + </field> + + <field name = "source" domain = "exchange-name" + label = "name of the source exchange to bind to"> + <doc>Specifies the name of the source exchange to bind.</doc> + <rule name = "exchange-existence" on-failure = "not-found"> + <doc> + A client MUST NOT be allowed to bind a non-existent source + exchange. + </doc> + <doc type = "scenario"> + A client attempts to bind an exchange to an undeclared + exchange. + </doc> + </rule> + <rule name = "default-exchange"> + <doc> + The server MUST accept a blank exchange name to mean the + default exchange. + </doc> + <doc type = "scenario"> + The client declares an exchange and binds it to a blank + exchange name. + </doc> + </rule> + </field> + + <field name = "routing-key" domain = "shortstr" + label = "message routing key"> + <doc> + Specifies the routing key for the binding. The routing key + is used for routing messages depending on the exchange + configuration. Not all exchanges use a routing key - refer + to the specific exchange documentation. + </doc> + </field> + + <field name = "no-wait" domain = "no-wait"/> + + <field name = "arguments" domain = "table" + label = "arguments for binding"> + <doc> + A set of arguments for the binding. The syntax and semantics + of these arguments depends on the exchange class. + </doc> + </field> + </method> + + <method name="bind-ok" synchronous="1" index="31" + label = "confirm bind successful"> + <doc>This method confirms that the bind was successful.</doc> + + <chassis name="client" implement="MUST"/> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "unbind" synchronous = "1" index = "40" + label = "unbind an exchange from an exchange"> + <doc>This method unbinds an exchange from an exchange.</doc> + <rule name = "01"> + <doc>If a unbind fails, the server MUST raise a connection exception.</doc> + </rule> + <chassis name = "server" implement = "MUST"/> + <response name = "unbind-ok"/> + + <!-- Deprecated: "ticket", must be zero --> + <field name = "reserved-1" type = "short" reserved = "1"/> + + <field name = "destination" domain = "exchange-name"> + <doc>Specifies the name of the destination exchange to unbind.</doc> + <rule name = "must-exist" on-failure = "not-found"> + <doc> + The client MUST NOT attempt to unbind an exchange that + does not exist from an exchange. + </doc> + <doc type = "scenario"> + The client attempts to unbind a non-existent exchange from + an exchange. + </doc> + </rule> + <rule name = "default-exchange"> + <doc> + The server MUST accept a blank exchange name to mean the + default exchange. + </doc> + <doc type = "scenario"> + The client declares an exchange, binds a blank exchange + name to it, and then unbinds a blank exchange name from + it. + </doc> + </rule> + </field> + + <field name = "source" domain = "exchange-name"> + <doc>Specifies the name of the source exchange to unbind.</doc> + <rule name = "must-exist" on-failure = "not-found"> + <doc> + The client MUST NOT attempt to unbind an exchange from an + exchange that does not exist. + </doc> + <doc type = "scenario"> + The client attempts to unbind an exchange from a + non-existent exchange. + </doc> + </rule> + <rule name = "default-exchange"> + <doc> + The server MUST accept a blank exchange name to mean the + default exchange. + </doc> + <doc type = "scenario"> + The client declares an exchange, binds an exchange to a + blank exchange name, and then unbinds an exchange from a + black exchange name. + </doc> + </rule> + </field> + + <field name = "routing-key" domain = "shortstr" + label = "routing key of binding"> + <doc>Specifies the routing key of the binding to unbind.</doc> + </field> + + <field name = "no-wait" domain = "no-wait"/> + + <field name = "arguments" domain = "table" + label = "arguments of binding"> + <doc>Specifies the arguments of the binding to unbind.</doc> + </field> + </method> + + <method name = "unbind-ok" synchronous = "1" index = "51" + label = "confirm unbind successful"> + <doc>This method confirms that the unbind was successful.</doc> + <chassis name = "client" implement = "MUST"/> + </method> + + </class> + + <!-- == QUEUE ============================================================ --> + + <class name = "queue" handler = "channel" index = "50" label = "work with queues"> + <doc> + Queues store and forward messages. Queues can be configured in the server or created at + runtime. Queues must be attached to at least one exchange in order to receive messages + from publishers. + </doc> + + <doc type = "grammar"> + queue = C:DECLARE S:DECLARE-OK + / C:BIND S:BIND-OK + / C:UNBIND S:UNBIND-OK + / C:PURGE S:PURGE-OK + / C:DELETE S:DELETE-OK + </doc> + + <chassis name = "server" implement = "MUST" /> + <chassis name = "client" implement = "MUST" /> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "declare" synchronous = "1" index = "10" label = "declare queue, create if needed"> + <doc> + This method creates or checks a queue. When creating a new queue the client can + specify various properties that control the durability of the queue and its + contents, and the level of sharing for the queue. + </doc> + + <rule name = "default-binding"> + <doc> + The server MUST create a default binding for a newly-declared queue to the + default exchange, which is an exchange of type 'direct' and use the queue + name as the routing key. + </doc> + <doc type = "scenario"> + Client declares a new queue, and then without explicitly binding it to an + exchange, attempts to send a message through the default exchange binding, + i.e. publish a message to the empty exchange, with the queue name as routing + key. + </doc> + </rule> + + <rule name = "minimum-queues"> + <doc> + The server SHOULD support a minimum of 256 queues per virtual host and ideally, + impose no limit except as defined by available resources. + </doc> + <doc type = "scenario"> + Client attempts to declare as many queues as it can until the server reports + an error. The resulting count must at least be 256. + </doc> + </rule> + + <chassis name = "server" implement = "MUST" /> + <response name = "declare-ok" /> + + <!-- Deprecated: "ticket", must be zero --> + <field name = "reserved-1" type = "short" reserved = "1" /> + + <field name = "queue" domain = "queue-name"> + <rule name = "default-name"> + <doc> + The queue name MAY be empty, in which case the server MUST create a new + queue with a unique generated name and return this to the client in the + Declare-Ok method. + </doc> + <doc type = "scenario"> + Client attempts to declare several queues with an empty name. The client then + verifies that the server-assigned names are unique and different. + </doc> + </rule> + <rule name = "reserved" on-failure = "access-refused"> + <doc> + Queue names starting with "amq." are reserved for pre-declared and + standardised queues. The client MAY declare a queue starting with + "amq." if the passive option is set, or the queue already exists. + </doc> + <doc type = "scenario"> + The client attempts to declare a non-existing queue starting with + "amq." and with the passive option set to zero. + </doc> + </rule> + <rule name = "syntax" on-failure = "precondition-failed"> + <doc> + The queue name can be empty, or a sequence of these characters: + letters, digits, hyphen, underscore, period, or colon. + </doc> + <doc type = "scenario"> + The client attempts to declare a queue with an illegal name. + </doc> + </rule> + </field> + + <field name = "passive" domain = "bit" label = "do not create queue"> + <doc> + If set, the server will reply with Declare-Ok if the queue already + exists with the same name, and raise an error if not. The client can + use this to check whether a queue exists without modifying the + server state. When set, all other method fields except name and no-wait + are ignored. A declare with both passive and no-wait has no effect. + Arguments are compared for semantic equivalence. + </doc> + <rule name = "passive" on-failure = "not-found"> + <doc> + The client MAY ask the server to assert that a queue exists without + creating the queue if not. If the queue does not exist, the server + treats this as a failure. + </doc> + <doc type = "scenario"> + Client declares an existing queue with the passive option and expects + the server to respond with a declare-ok. Client then attempts to declare + a non-existent queue with the passive option, and the server must close + the channel with the correct reply-code. + </doc> + </rule> + <rule name = "equivalent"> + <doc> + If not set and the queue exists, the server MUST check that the + existing queue has the same values for durable, exclusive, auto-delete, + and arguments fields. The server MUST respond with Declare-Ok if the + requested queue matches these fields, and MUST raise a channel exception + if not. + </doc> + <doc type = "scenario"> + TODO. + </doc> + </rule> + </field> + + <field name = "durable" domain = "bit" label = "request a durable queue"> + <doc> + If set when creating a new queue, the queue will be marked as durable. Durable + queues remain active when a server restarts. Non-durable queues (transient + queues) are purged if/when a server restarts. Note that durable queues do not + necessarily hold persistent messages, although it does not make sense to send + persistent messages to a transient queue. + </doc> + + <rule name = "persistence"> + <doc>The server MUST recreate the durable queue after a restart.</doc> + + <doc type = "scenario"> + Client declares a durable queue. The server is then restarted. The client + then attempts to send a message to the queue. The message should be successfully + delivered. + </doc> + </rule> + + <rule name = "types"> + <doc>The server MUST support both durable and transient queues.</doc> + <doc type = "scenario"> + A client declares two named queues, one durable and one transient. + </doc> + </rule> + </field> + + <field name = "exclusive" domain = "bit" label = "request an exclusive queue"> + <doc> + Exclusive queues may only be accessed by the current connection, and are + deleted when that connection closes. Passive declaration of an exclusive + queue by other connections are not allowed. + </doc> + + <rule name = "types"> + <doc> + The server MUST support both exclusive (private) and non-exclusive (shared) + queues. + </doc> + <doc type = "scenario"> + A client declares two named queues, one exclusive and one non-exclusive. + </doc> + </rule> + + <rule name = "exclusive" on-failure = "resource-locked"> + <doc> + The client MAY NOT attempt to use a queue that was declared as exclusive + by another still-open connection. + </doc> + <doc type = "scenario"> + One client declares an exclusive queue. A second client on a different + connection attempts to declare, bind, consume, purge, delete, or declare + a queue of the same name. + </doc> + </rule> + </field> + + <field name = "auto-delete" domain = "bit" label = "auto-delete queue when unused"> + <doc> + If set, the queue is deleted when all consumers have finished using it. The last + consumer can be cancelled either explicitly or because its channel is closed. If + there was no consumer ever on the queue, it won't be deleted. Applications can + explicitly delete auto-delete queues using the Delete method as normal. + </doc> + + <rule name = "pre-existence"> + <doc> + The server MUST ignore the auto-delete field if the queue already exists. + </doc> + <doc type = "scenario"> + Client declares two named queues, one as auto-delete and one explicit-delete. + Client then attempts to declare the two queues using the same names again, + but reversing the value of the auto-delete field in each case. Verify that the + queues still exist with the original auto-delete flag values. + </doc> + </rule> + </field> + + <field name = "no-wait" domain = "no-wait" /> + + <field name = "arguments" domain = "table" label = "arguments for declaration"> + <doc> + A set of arguments for the declaration. The syntax and semantics of these + arguments depends on the server implementation. + </doc> + </field> + </method> + + <method name = "declare-ok" synchronous = "1" index = "11" label = "confirms a queue definition"> + <doc> + This method confirms a Declare method and confirms the name of the queue, essential + for automatically-named queues. + </doc> + + <chassis name = "client" implement = "MUST" /> + + <field name = "queue" domain = "queue-name"> + <doc> + Reports the name of the queue. If the server generated a queue name, this field + contains that name. + </doc> + <assert check = "notnull" /> + </field> + + <field name = "message-count" domain = "message-count" /> + + <field name = "consumer-count" domain = "long" label = "number of consumers"> + <doc> + Reports the number of active consumers for the queue. Note that consumers can + suspend activity (Channel.Flow) in which case they do not appear in this count. + </doc> + </field> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "bind" synchronous = "1" index = "20" label = "bind queue to an exchange"> + <doc> + This method binds a queue to an exchange. Until a queue is bound it will not + receive any messages. In a classic messaging model, store-and-forward queues + are bound to a direct exchange and subscription queues are bound to a topic + exchange. + </doc> + + <rule name = "duplicates"> + <doc> + A server MUST allow ignore duplicate bindings - that is, two or more bind + methods for a specific queue, with identical arguments - without treating these + as an error. + </doc> + <doc type = "scenario"> + A client binds a named queue to an exchange. The client then repeats the bind + (with identical arguments). + </doc> + </rule> + + <rule name = "unique"> + <doc> + A server MUST not deliver the same message more than once to a queue, even if + the queue has multiple bindings that match the message. + </doc> + <doc type = "scenario"> + A client declares a named queue and binds it using multiple bindings to the + amq.topic exchange. The client then publishes a message that matches all its + bindings. + </doc> + </rule> + + <rule name = "transient-exchange"> + <doc> + The server MUST allow a durable queue to bind to a transient exchange. + </doc> + <doc type = "scenario"> + A client declares a transient exchange. The client then declares a named durable + queue and then attempts to bind the transient exchange to the durable queue. + </doc> + </rule> + + <rule name = "durable-exchange"> + <doc> + Bindings of durable queues to durable exchanges are automatically durable + and the server MUST restore such bindings after a server restart. + </doc> + <doc type = "scenario"> + A server declares a named durable queue and binds it to a durable exchange. The + server is restarted. The client then attempts to use the queue/exchange combination. + </doc> + </rule> + + <rule name = "binding-count"> + <doc> + The server SHOULD support at least 4 bindings per queue, and ideally, impose no + limit except as defined by available resources. + </doc> + <doc type = "scenario"> + A client declares a named queue and attempts to bind it to 4 different + exchanges. + </doc> + </rule> + + <chassis name = "server" implement = "MUST" /> + + <response name = "bind-ok" /> + + <!-- Deprecated: "ticket", must be zero --> + <field name = "reserved-1" type = "short" reserved = "1" /> + + <field name = "queue" domain = "queue-name"> + <doc>Specifies the name of the queue to bind.</doc> + <rule name = "queue-known" on-failure = "not-found"> + <doc> + The client MUST either specify a queue name or have previously declared a + queue on the same channel + </doc> + <doc type = "scenario"> + The client opens a channel and attempts to bind an unnamed queue. + </doc> + </rule> + <rule name = "must-exist" on-failure = "not-found"> + <doc> + The client MUST NOT attempt to bind a queue that does not exist. + </doc> + <doc type = "scenario"> + The client attempts to bind a non-existent queue. + </doc> + </rule> + </field> + + <field name = "exchange" domain = "exchange-name" label = "name of the exchange to bind to"> + <rule name = "exchange-existence" on-failure = "not-found"> + <doc> + A client MUST NOT be allowed to bind a queue to a non-existent exchange. + </doc> + <doc type = "scenario"> + A client attempts to bind an named queue to a undeclared exchange. + </doc> + </rule> + <rule name = "default-exchange"> + <doc> + The server MUST accept a blank exchange name to mean the default exchange. + </doc> + <doc type = "scenario"> + The client declares a queue and binds it to a blank exchange name. + </doc> + </rule> + </field> + + <field name = "routing-key" domain = "shortstr" label = "message routing key"> + <doc> + Specifies the routing key for the binding. The routing key is used for routing + messages depending on the exchange configuration. Not all exchanges use a + routing key - refer to the specific exchange documentation. If the queue name + is empty, the server uses the last queue declared on the channel. If the + routing key is also empty, the server uses this queue name for the routing + key as well. If the queue name is provided but the routing key is empty, the + server does the binding with that empty routing key. The meaning of empty + routing keys depends on the exchange implementation. + </doc> + <rule name = "direct-exchange-key-matching"> + <doc> + If a message queue binds to a direct exchange using routing key K and a + publisher sends the exchange a message with routing key R, then the message + MUST be passed to the message queue if K = R. + </doc> + </rule> + </field> + + <field name = "no-wait" domain = "no-wait" /> + + <field name = "arguments" domain = "table" label = "arguments for binding"> + <doc> + A set of arguments for the binding. The syntax and semantics of these arguments + depends on the exchange class. + </doc> + </field> + </method> + + <method name = "bind-ok" synchronous = "1" index = "21" label = "confirm bind successful"> + <doc>This method confirms that the bind was successful.</doc> + + <chassis name = "client" implement = "MUST" /> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "unbind" synchronous = "1" index = "50" label = "unbind a queue from an exchange"> + <doc>This method unbinds a queue from an exchange.</doc> + <rule name = "01"> + <doc>If a unbind fails, the server MUST raise a connection exception.</doc> + </rule> + <chassis name="server" implement="MUST"/> + <response name="unbind-ok"/> + + <!-- Deprecated: "ticket", must be zero --> + <field name = "reserved-1" type = "short" reserved = "1" /> + + <field name = "queue" domain = "queue-name"> + <doc>Specifies the name of the queue to unbind.</doc> + <rule name = "queue-known" on-failure = "not-found"> + <doc> + The client MUST either specify a queue name or have previously declared a + queue on the same channel + </doc> + <doc type = "scenario"> + The client opens a channel and attempts to unbind an unnamed queue. + </doc> + </rule> + <rule name = "must-exist" on-failure = "not-found"> + <doc> + The client MUST NOT attempt to unbind a queue that does not exist. + </doc> + <doc type = "scenario"> + The client attempts to unbind a non-existent queue. + </doc> + </rule> + </field> + + <field name = "exchange" domain = "exchange-name"> + <doc>The name of the exchange to unbind from.</doc> + <rule name = "must-exist" on-failure = "not-found"> + <doc> + The client MUST NOT attempt to unbind a queue from an exchange that + does not exist. + </doc> + <doc type = "scenario"> + The client attempts to unbind a queue from a non-existent exchange. + </doc> + </rule> + <rule name = "default-exchange"> + <doc> + The server MUST accept a blank exchange name to mean the default exchange. + </doc> + <doc type = "scenario"> + The client declares a queue and binds it to a blank exchange name. + </doc> + </rule> + </field> + + <field name = "routing-key" domain = "shortstr" label = "routing key of binding"> + <doc>Specifies the routing key of the binding to unbind.</doc> + </field> + + <field name = "arguments" domain = "table" label = "arguments of binding"> + <doc>Specifies the arguments of the binding to unbind.</doc> + </field> + </method> + + <method name = "unbind-ok" synchronous = "1" index = "51" label = "confirm unbind successful"> + <doc>This method confirms that the unbind was successful.</doc> + <chassis name = "client" implement = "MUST"/> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "purge" synchronous = "1" index = "30" label = "purge a queue"> + <doc> + This method removes all messages from a queue which are not awaiting + acknowledgment. + </doc> + + <rule name = "02"> + <doc> + The server MUST NOT purge messages that have already been sent to a client + but not yet acknowledged. + </doc> + </rule> + + <rule name = "03"> + <doc> + The server MAY implement a purge queue or log that allows system administrators + to recover accidentally-purged messages. The server SHOULD NOT keep purged + messages in the same storage spaces as the live messages since the volumes of + purged messages may get very large. + </doc> + </rule> + + <chassis name = "server" implement = "MUST" /> + + <response name = "purge-ok" /> + + <!-- Deprecated: "ticket", must be zero --> + <field name = "reserved-1" type = "short" reserved = "1" /> + + <field name = "queue" domain = "queue-name"> + <doc>Specifies the name of the queue to purge.</doc> + <rule name = "queue-known" on-failure = "not-found"> + <doc> + The client MUST either specify a queue name or have previously declared a + queue on the same channel + </doc> + <doc type = "scenario"> + The client opens a channel and attempts to purge an unnamed queue. + </doc> + </rule> + <rule name = "must-exist" on-failure = "not-found"> + <doc> + The client MUST NOT attempt to purge a queue that does not exist. + </doc> + <doc type = "scenario"> + The client attempts to purge a non-existent queue. + </doc> + </rule> + </field> + + <field name = "no-wait" domain = "no-wait" /> + </method> + + <method name = "purge-ok" synchronous = "1" index = "31" label = "confirms a queue purge"> + <doc>This method confirms the purge of a queue.</doc> + + <chassis name = "client" implement = "MUST" /> + + <field name = "message-count" domain = "message-count"> + <doc> + Reports the number of messages purged. + </doc> + </field> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "delete" synchronous = "1" index = "40" label = "delete a queue"> + <doc> + This method deletes a queue. When a queue is deleted any pending messages are sent + to a dead-letter queue if this is defined in the server configuration, and all + consumers on the queue are cancelled. + </doc> + + <rule name = "01"> + <doc> + The server SHOULD use a dead-letter queue to hold messages that were pending on + a deleted queue, and MAY provide facilities for a system administrator to move + these messages back to an active queue. + </doc> + </rule> + + <chassis name = "server" implement = "MUST" /> + + <response name = "delete-ok" /> + + <!-- Deprecated: "ticket", must be zero --> + <field name = "reserved-1" type = "short" reserved = "1" /> + + <field name = "queue" domain = "queue-name"> + <doc>Specifies the name of the queue to delete.</doc> + <rule name = "queue-known" on-failure = "not-found"> + <doc> + The client MUST either specify a queue name or have previously declared a + queue on the same channel + </doc> + <doc type = "scenario"> + The client opens a channel and attempts to delete an unnamed queue. + </doc> + </rule> + <rule name = "must-exist" on-failure = "not-found"> + <doc> + The client MUST NOT attempt to delete a queue that does not exist. + </doc> + <doc type = "scenario"> + The client attempts to delete a non-existent queue. + </doc> + </rule> + </field> + + <field name = "if-unused" domain = "bit" label = "delete only if unused"> + <doc> + If set, the server will only delete the queue if it has no consumers. If the + queue has consumers the server does does not delete it but raises a channel + exception instead. + </doc> + <rule name = "in-use" on-failure = "precondition-failed"> + <doc> + The server MUST NOT delete a queue that has consumers on it, if the if-unused + field is true. + </doc> + <doc type = "scenario"> + The client declares a queue, and consumes from it, then tries to delete it + setting if-unused to true. + </doc> + </rule> + </field> + + <field name = "if-empty" domain = "bit" label = "delete only if empty"> + <doc> + If set, the server will only delete the queue if it has no messages. + </doc> + <rule name = "not-empty" on-failure = "precondition-failed"> + <doc> + The server MUST NOT delete a queue that has messages on it, if the + if-empty field is true. + </doc> + <doc type = "scenario"> + The client declares a queue, binds it and publishes some messages into it, + then tries to delete it setting if-empty to true. + </doc> + </rule> + </field> + + <field name = "no-wait" domain = "no-wait" /> + </method> + + <method name = "delete-ok" synchronous = "1" index = "41" label = "confirm deletion of a queue"> + <doc>This method confirms the deletion of a queue.</doc> + + <chassis name = "client" implement = "MUST" /> + + <field name = "message-count" domain = "message-count"> + <doc>Reports the number of messages deleted.</doc> + </field> + </method> + </class> + + <!-- == BASIC ============================================================ --> + + <class name = "basic" handler = "channel" index = "60" label = "work with basic content"> + <doc> + The Basic class provides methods that support an industry-standard messaging model. + </doc> + + <doc type = "grammar"> + basic = C:QOS S:QOS-OK + / C:CONSUME S:CONSUME-OK + / C:CANCEL S:CANCEL-OK + / C:PUBLISH content + / S:RETURN content + / S:DELIVER content + / C:GET ( S:GET-OK content / S:GET-EMPTY ) + / C:ACK + / S:ACK + / C:REJECT + / C:NACK + / S:NACK + / C:RECOVER-ASYNC + / C:RECOVER S:RECOVER-OK + </doc> + + <chassis name = "server" implement = "MUST" /> + <chassis name = "client" implement = "MAY" /> + + <rule name = "01"> + <doc> + The server SHOULD respect the persistent property of basic messages and + SHOULD make a best-effort to hold persistent basic messages on a reliable + storage mechanism. + </doc> + <doc type = "scenario"> + Send a persistent message to queue, stop server, restart server and then + verify whether message is still present. Assumes that queues are durable. + Persistence without durable queues makes no sense. + </doc> + </rule> + + <rule name = "02"> + <doc> + The server MUST NOT discard a persistent basic message in case of a queue + overflow. + </doc> + <doc type = "scenario"> + Declare a queue overflow situation with persistent messages and verify that + messages do not get lost (presumably the server will write them to disk). + </doc> + </rule> + + <rule name = "03"> + <doc> + The server MAY use the Channel.Flow method to slow or stop a basic message + publisher when necessary. + </doc> + <doc type = "scenario"> + Declare a queue overflow situation with non-persistent messages and verify + whether the server responds with Channel.Flow or not. Repeat with persistent + messages. + </doc> + </rule> + + <rule name = "04"> + <doc> + The server MAY overflow non-persistent basic messages to persistent + storage. + </doc> + <!-- Test scenario: untestable --> + </rule> + + <rule name = "05"> + <doc> + The server MAY discard or dead-letter non-persistent basic messages on a + priority basis if the queue size exceeds some configured limit. + </doc> + <!-- Test scenario: untestable --> + </rule> + + <rule name = "06"> + <doc> + The server MUST implement at least 2 priority levels for basic messages, + where priorities 0-4 and 5-9 are treated as two distinct levels. + </doc> + <doc type = "scenario"> + Send a number of priority 0 messages to a queue. Send one priority 9 + message. Consume messages from the queue and verify that the first message + received was priority 9. + </doc> + </rule> + + <rule name = "07"> + <doc> + The server MAY implement up to 10 priority levels. + </doc> + <doc type = "scenario"> + Send a number of messages with mixed priorities to a queue, so that all + priority values from 0 to 9 are exercised. A good scenario would be ten + messages in low-to-high priority. Consume from queue and verify how many + priority levels emerge. + </doc> + </rule> + + <rule name = "08"> + <doc> + The server MUST deliver messages of the same priority in order irrespective of + their individual persistence. + </doc> + <doc type = "scenario"> + Send a set of messages with the same priority but different persistence + settings to a queue. Consume and verify that messages arrive in same order + as originally published. + </doc> + </rule> + + <rule name = "09"> + <doc> + The server MUST support un-acknowledged delivery of Basic content, i.e. + consumers with the no-ack field set to TRUE. + </doc> + </rule> + + <rule name = "10"> + <doc> + The server MUST support explicitly acknowledged delivery of Basic content, + i.e. consumers with the no-ack field set to FALSE. + </doc> + <doc type = "scenario"> + Declare a queue and a consumer using explicit acknowledgements. Publish a + set of messages to the queue. Consume the messages but acknowledge only + half of them. Disconnect and reconnect, and consume from the queue. + Verify that the remaining messages are received. + </doc> + </rule> + + <!-- These are the properties for a Basic content --> + + <!-- MIME typing --> + <field name = "content-type" domain = "shortstr" label = "MIME content type" /> + <!-- MIME typing --> + <field name = "content-encoding" domain = "shortstr" label = "MIME content encoding" /> + <!-- For applications, and for header exchange routing --> + <field name = "headers" domain = "table" label = "message header field table" /> + <!-- For queues that implement persistence --> + <field name = "delivery-mode" domain = "octet" label = "non-persistent (1) or persistent (2)" /> + <!-- For queues that implement priorities --> + <field name = "priority" domain = "octet" label = "message priority, 0 to 9" /> + <!-- For application use, no formal behaviour --> + <field name = "correlation-id" domain = "shortstr" label = "application correlation identifier" /> + <!-- For application use, no formal behaviour but may hold the + name of a private response queue, when used in request messages --> + <field name = "reply-to" domain = "shortstr" label = "address to reply to" /> + <!-- For implementation use, no formal behaviour --> + <field name = "expiration" domain = "shortstr" label = "message expiration specification" /> + <!-- For application use, no formal behaviour --> + <field name = "message-id" domain = "shortstr" label = "application message identifier" /> + <!-- For application use, no formal behaviour --> + <field name = "timestamp" domain = "timestamp" label = "message timestamp" /> + <!-- For application use, no formal behaviour --> + <field name = "type" domain = "shortstr" label = "message type name" /> + <!-- For application use, no formal behaviour --> + <field name = "user-id" domain = "shortstr" label = "creating user id" /> + <!-- For application use, no formal behaviour --> + <field name = "app-id" domain = "shortstr" label = "creating application id" /> + <!-- Deprecated, was old cluster-id property --> + <field name = "reserved" domain = "shortstr" label = "reserved, must be empty" /> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "qos" synchronous = "1" index = "10" label = "specify quality of service"> + <doc> + This method requests a specific quality of service. The QoS can be specified for the + current channel or for all channels on the connection. The particular properties and + semantics of a qos method always depend on the content class semantics. Though the + qos method could in principle apply to both peers, it is currently meaningful only + for the server. + </doc> + + <chassis name = "server" implement = "MUST" /> + <response name = "qos-ok" /> + + <field name = "prefetch-size" domain = "long" label = "prefetch window in octets"> + <doc> + The client can request that messages be sent in advance so that when the client + finishes processing a message, the following message is already held locally, + rather than needing to be sent down the channel. Prefetching gives a performance + improvement. This field specifies the prefetch window size in octets. The server + will send a message in advance if it is equal to or smaller in size than the + available prefetch size (and also falls into other prefetch limits). May be set + to zero, meaning "no specific limit", although other prefetch limits may still + apply. The prefetch-size is ignored if the no-ack option is set. + </doc> + <rule name = "01"> + <doc> + The server MUST ignore this setting when the client is not processing any + messages - i.e. the prefetch size does not limit the transfer of single + messages to a client, only the sending in advance of more messages while + the client still has one or more unacknowledged messages. + </doc> + <doc type = "scenario"> + Define a QoS prefetch-size limit and send a single message that exceeds + that limit. Verify that the message arrives correctly. + </doc> + </rule> + </field> + + <field name = "prefetch-count" domain = "short" label = "prefetch window in messages"> + <doc> + Specifies a prefetch window in terms of whole messages. This field may be used + in combination with the prefetch-size field; a message will only be sent in + advance if both prefetch windows (and those at the channel and connection level) + allow it. The prefetch-count is ignored if the no-ack option is set. + </doc> + <rule name = "01"> + <doc> + The server may send less data in advance than allowed by the client's + specified prefetch windows but it MUST NOT send more. + </doc> + <doc type = "scenario"> + Define a QoS prefetch-size limit and a prefetch-count limit greater than + one. Send multiple messages that exceed the prefetch size. Verify that + no more than one message arrives at once. + </doc> + </rule> + </field> + + <field name = "global" domain = "bit" label = "apply to entire connection"> + <doc> + RabbitMQ has reinterpreted this field. The original + specification said: "By default the QoS settings apply to + the current channel only. If this field is set, they are + applied to the entire connection." Instead, RabbitMQ takes + global=false to mean that the QoS settings should apply + per-consumer (for new consumers on the channel; existing + ones being unaffected) and global=true to mean that the QoS + settings should apply per-channel. + </doc> + </field> + </method> + + <method name = "qos-ok" synchronous = "1" index = "11" label = "confirm the requested qos"> + <doc> + This method tells the client that the requested QoS levels could be handled by the + server. The requested QoS applies to all active consumers until a new QoS is + defined. + </doc> + <chassis name = "client" implement = "MUST" /> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "consume" synchronous = "1" index = "20" label = "start a queue consumer"> + <doc> + This method asks the server to start a "consumer", which is a transient request for + messages from a specific queue. Consumers last as long as the channel they were + declared on, or until the client cancels them. + </doc> + + <rule name = "01"> + <doc> + The server SHOULD support at least 16 consumers per queue, and ideally, impose + no limit except as defined by available resources. + </doc> + <doc type = "scenario"> + Declare a queue and create consumers on that queue until the server closes the + connection. Verify that the number of consumers created was at least sixteen + and report the total number. + </doc> + </rule> + + <chassis name = "server" implement = "MUST" /> + <response name = "consume-ok" /> + + <!-- Deprecated: "ticket", must be zero --> + <field name = "reserved-1" type = "short" reserved = "1" /> + + <field name = "queue" domain = "queue-name"> + <doc>Specifies the name of the queue to consume from.</doc> + </field> + + <field name = "consumer-tag" domain = "consumer-tag"> + <doc> + Specifies the identifier for the consumer. The consumer tag is local to a + channel, so two clients can use the same consumer tags. If this field is + empty the server will generate a unique tag. + </doc> + <rule name = "01" on-failure = "not-allowed"> + <doc> + The client MUST NOT specify a tag that refers to an existing consumer. + </doc> + <doc type = "scenario"> + Attempt to create two consumers with the same non-empty tag, on the + same channel. + </doc> + </rule> + <rule name = "02" on-failure = "not-allowed"> + <doc> + The consumer tag is valid only within the channel from which the + consumer was created. I.e. a client MUST NOT create a consumer in one + channel and then use it in another. + </doc> + <doc type = "scenario"> + Attempt to create a consumer in one channel, then use in another channel, + in which consumers have also been created (to test that the server uses + unique consumer tags). + </doc> + </rule> + </field> + + <field name = "no-local" domain = "no-local" /> + + <field name = "no-ack" domain = "no-ack" /> + + <field name = "exclusive" domain = "bit" label = "request exclusive access"> + <doc> + Request exclusive consumer access, meaning only this consumer can access the + queue. + </doc> + + <rule name = "01" on-failure = "access-refused"> + <doc> + The client MAY NOT gain exclusive access to a queue that already has + active consumers. + </doc> + <doc type = "scenario"> + Open two connections to a server, and in one connection declare a shared + (non-exclusive) queue and then consume from the queue. In the second + connection attempt to consume from the same queue using the exclusive + option. + </doc> + </rule> + </field> + + <field name = "no-wait" domain = "no-wait" /> + + <field name = "arguments" domain = "table" label = "arguments for declaration"> + <doc> + A set of arguments for the consume. The syntax and semantics of these + arguments depends on the server implementation. + </doc> + </field> + </method> + + <method name = "consume-ok" synchronous = "1" index = "21" label = "confirm a new consumer"> + <doc> + The server provides the client with a consumer tag, which is used by the client + for methods called on the consumer at a later stage. + </doc> + <chassis name = "client" implement = "MUST" /> + <field name = "consumer-tag" domain = "consumer-tag"> + <doc> + Holds the consumer tag specified by the client or provided by the server. + </doc> + </field> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "cancel" synchronous = "1" index = "30" label = "end a queue consumer"> + <doc> + This method cancels a consumer. This does not affect already delivered + messages, but it does mean the server will not send any more messages for + that consumer. The client may receive an arbitrary number of messages in + between sending the cancel method and receiving the cancel-ok reply. + + It may also be sent from the server to the client in the event + of the consumer being unexpectedly cancelled (i.e. cancelled + for any reason other than the server receiving the + corresponding basic.cancel from the client). This allows + clients to be notified of the loss of consumers due to events + such as queue deletion. Note that as it is not a MUST for + clients to accept this method from the server, it is advisable + for the broker to be able to identify those clients that are + capable of accepting the method, through some means of + capability negotiation. + </doc> + + <rule name = "01"> + <doc> + If the queue does not exist the server MUST ignore the cancel method, so + long as the consumer tag is valid for that channel. + </doc> + <doc type = "scenario"> + TODO. + </doc> + </rule> + + <chassis name = "server" implement = "MUST" /> + <chassis name = "client" implement = "SHOULD" /> + <response name = "cancel-ok" /> + + <field name = "consumer-tag" domain = "consumer-tag" /> + <field name = "no-wait" domain = "no-wait" /> + </method> + + <method name = "cancel-ok" synchronous = "1" index = "31" label = "confirm a cancelled consumer"> + <doc> + This method confirms that the cancellation was completed. + </doc> + <chassis name = "client" implement = "MUST" /> + <chassis name = "server" implement = "MAY" /> + <field name = "consumer-tag" domain = "consumer-tag" /> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "publish" content = "1" index = "40" label = "publish a message"> + <doc> + This method publishes a message to a specific exchange. The message will be routed + to queues as defined by the exchange configuration and distributed to any active + consumers when the transaction, if any, is committed. + </doc> + + <chassis name = "server" implement = "MUST" /> + + <!-- Deprecated: "ticket", must be zero --> + <field name = "reserved-1" type = "short" reserved = "1" /> + + <field name = "exchange" domain = "exchange-name"> + <doc> + Specifies the name of the exchange to publish to. The exchange name can be + empty, meaning the default exchange. If the exchange name is specified, and that + exchange does not exist, the server will raise a channel exception. + </doc> + + <rule name = "must-exist" on-failure = "not-found"> + <doc> + The client MUST NOT attempt to publish a content to an exchange that + does not exist. + </doc> + <doc type = "scenario"> + The client attempts to publish a content to a non-existent exchange. + </doc> + </rule> + <rule name = "default-exchange"> + <doc> + The server MUST accept a blank exchange name to mean the default exchange. + </doc> + <doc type = "scenario"> + The client declares a queue and binds it to a blank exchange name. + </doc> + </rule> + <rule name = "02"> + <doc> + If the exchange was declared as an internal exchange, the server MUST raise + a channel exception with a reply code 403 (access refused). + </doc> + <doc type = "scenario"> + TODO. + </doc> + </rule> + + <rule name = "03"> + <doc> + The exchange MAY refuse basic content in which case it MUST raise a channel + exception with reply code 540 (not implemented). + </doc> + <doc type = "scenario"> + TODO. + </doc> + </rule> + </field> + + <field name = "routing-key" domain = "shortstr" label = "Message routing key"> + <doc> + Specifies the routing key for the message. The routing key is used for routing + messages depending on the exchange configuration. + </doc> + </field> + + <field name = "mandatory" domain = "bit" label = "indicate mandatory routing"> + <doc> + This flag tells the server how to react if the message cannot be routed to a + queue. If this flag is set, the server will return an unroutable message with a + Return method. If this flag is zero, the server silently drops the message. + </doc> + + <rule name = "01"> + <doc> + The server SHOULD implement the mandatory flag. + </doc> + <doc type = "scenario"> + TODO. + </doc> + </rule> + </field> + + <field name = "immediate" domain = "bit" label = "request immediate delivery"> + <doc> + This flag tells the server how to react if the message cannot be routed to a + queue consumer immediately. If this flag is set, the server will return an + undeliverable message with a Return method. If this flag is zero, the server + will queue the message, but with no guarantee that it will ever be consumed. + </doc> + + <rule name = "01"> + <doc> + The server SHOULD implement the immediate flag. + </doc> + <doc type = "scenario"> + TODO. + </doc> + </rule> + </field> + </method> + + <method name = "return" content = "1" index = "50" label = "return a failed message"> + <doc> + This method returns an undeliverable message that was published with the "immediate" + flag set, or an unroutable message published with the "mandatory" flag set. The + reply code and text provide information about the reason that the message was + undeliverable. + </doc> + + <chassis name = "client" implement = "MUST" /> + + <field name = "reply-code" domain = "reply-code" /> + <field name = "reply-text" domain = "reply-text" /> + + <field name = "exchange" domain = "exchange-name"> + <doc> + Specifies the name of the exchange that the message was originally published + to. May be empty, meaning the default exchange. + </doc> + </field> + + <field name = "routing-key" domain = "shortstr" label = "Message routing key"> + <doc> + Specifies the routing key name specified when the message was published. + </doc> + </field> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "deliver" content = "1" index = "60" + label = "notify the client of a consumer message"> + <doc> + This method delivers a message to the client, via a consumer. In the asynchronous + message delivery model, the client starts a consumer using the Consume method, then + the server responds with Deliver methods as and when messages arrive for that + consumer. + </doc> + + <rule name = "01"> + <doc> + The server SHOULD track the number of times a message has been delivered to + clients and when a message is redelivered a certain number of times - e.g. 5 + times - without being acknowledged, the server SHOULD consider the message to be + unprocessable (possibly causing client applications to abort), and move the + message to a dead letter queue. + </doc> + <doc type = "scenario"> + TODO. + </doc> + </rule> + + <chassis name = "client" implement = "MUST" /> + + <field name = "consumer-tag" domain = "consumer-tag" /> + <field name = "delivery-tag" domain = "delivery-tag" /> + <field name = "redelivered" domain = "redelivered" /> + + <field name = "exchange" domain = "exchange-name"> + <doc> + Specifies the name of the exchange that the message was originally published to. + May be empty, indicating the default exchange. + </doc> + </field> + + <field name = "routing-key" domain = "shortstr" label = "Message routing key"> + <doc>Specifies the routing key name specified when the message was published.</doc> + </field> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "get" synchronous = "1" index = "70" label = "direct access to a queue"> + <doc> + This method provides a direct access to the messages in a queue using a synchronous + dialogue that is designed for specific types of application where synchronous + functionality is more important than performance. + </doc> + + <response name = "get-ok" /> + <response name = "get-empty" /> + <chassis name = "server" implement = "MUST" /> + + <!-- Deprecated: "ticket", must be zero --> + <field name = "reserved-1" type = "short" reserved = "1" /> + + <field name = "queue" domain = "queue-name"> + <doc>Specifies the name of the queue to get a message from.</doc> + </field> + <field name = "no-ack" domain = "no-ack" /> + </method> + + <method name = "get-ok" synchronous = "1" content = "1" index = "71" + label = "provide client with a message"> + <doc> + This method delivers a message to the client following a get method. A message + delivered by 'get-ok' must be acknowledged unless the no-ack option was set in the + get method. + </doc> + + <chassis name = "client" implement = "MAY" /> + + <field name = "delivery-tag" domain = "delivery-tag" /> + <field name = "redelivered" domain = "redelivered" /> + <field name = "exchange" domain = "exchange-name"> + <doc> + Specifies the name of the exchange that the message was originally published to. + If empty, the message was published to the default exchange. + </doc> + </field> + + <field name = "routing-key" domain = "shortstr" label = "Message routing key"> + <doc>Specifies the routing key name specified when the message was published.</doc> + </field> + + <field name = "message-count" domain = "message-count" /> + </method> + + <method name = "get-empty" synchronous = "1" index = "72" + label = "indicate no messages available"> + <doc> + This method tells the client that the queue has no messages available for the + client. + </doc> + <chassis name = "client" implement = "MAY" /> + <!-- Deprecated: "cluster-id", must be empty --> + <field name = "reserved-1" type = "shortstr" reserved = "1" /> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "ack" index = "80" label = "acknowledge one or more messages"> + <doc> + When sent by the client, this method acknowledges one or more + messages delivered via the Deliver or Get-Ok methods. + + When sent by server, this method acknowledges one or more + messages published with the Publish method on a channel in + confirm mode. + + The acknowledgement can be for a single message or a set of + messages up to and including a specific message. + </doc> + + <chassis name = "server" implement = "MUST" /> + <chassis name ="client" implement = "MUST"/> + + <field name = "delivery-tag" domain = "delivery-tag" /> + <field name = "multiple" domain = "bit" label = "acknowledge multiple messages"> + <doc> + If set to 1, the delivery tag is treated as "up to and + including", so that multiple messages can be acknowledged + with a single method. If set to zero, the delivery tag + refers to a single message. If the multiple field is 1, and + the delivery tag is zero, this indicates acknowledgement of + all outstanding messages. + </doc> + <rule name = "exists" on-failure = "precondition-failed"> + <doc> + A message MUST not be acknowledged more than once. The + receiving peer MUST validate that a non-zero delivery-tag + refers to a delivered message, and raise a channel + exception if this is not the case. On a transacted + channel, this check MUST be done immediately and not + delayed until a Tx.Commit. + </doc> + <doc type = "scenario"> + TODO. + </doc> + </rule> + </field> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "reject" index = "90" label = "reject an incoming message"> + <doc> + This method allows a client to reject a message. It can be used to interrupt and + cancel large incoming messages, or return untreatable messages to their original + queue. + </doc> + + <rule name = "01"> + <doc> + The server SHOULD be capable of accepting and process the Reject method while + sending message content with a Deliver or Get-Ok method. I.e. the server should + read and process incoming methods while sending output frames. To cancel a + partially-send content, the server sends a content body frame of size 1 (i.e. + with no data except the frame-end octet). + </doc> + </rule> + + <rule name = "02"> + <doc> + The server SHOULD interpret this method as meaning that the client is unable to + process the message at this time. + </doc> + <doc type = "scenario"> + TODO. + </doc> + </rule> + + <rule name = "03"> + <doc> + The client MUST NOT use this method as a means of selecting messages to process. + </doc> + <doc type = "scenario"> + TODO. + </doc> + </rule> + + <chassis name = "server" implement = "MUST" /> + + <field name = "delivery-tag" domain = "delivery-tag" /> + + <field name = "requeue" domain = "bit" label = "requeue the message"> + <doc> + If requeue is true, the server will attempt to requeue the message. If requeue + is false or the requeue attempt fails the messages are discarded or dead-lettered. + </doc> + + <rule name = "01"> + <doc> + The server MUST NOT deliver the message to the same client within the + context of the current channel. The recommended strategy is to attempt to + deliver the message to an alternative consumer, and if that is not possible, + to move the message to a dead-letter queue. The server MAY use more + sophisticated tracking to hold the message on the queue and redeliver it to + the same client at a later stage. + </doc> + <doc type = "scenario"> + TODO. + </doc> + </rule> + </field> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "recover-async" index = "100" label = "redeliver unacknowledged messages" + deprecated = "1"> + <doc> + This method asks the server to redeliver all unacknowledged messages on a + specified channel. Zero or more messages may be redelivered. This method + is deprecated in favour of the synchronous Recover/Recover-Ok. + </doc> + <rule name = "01"> + <doc> + The server MUST set the redelivered flag on all messages that are resent. + </doc> + <doc type = "scenario"> + TODO. + </doc> + </rule> + <chassis name = "server" implement = "MAY" /> + <field name = "requeue" domain = "bit" label = "requeue the message"> + <doc> + If this field is zero, the message will be redelivered to the original + recipient. If this bit is 1, the server will attempt to requeue the message, + potentially then delivering it to an alternative subscriber. + </doc> + </field> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "recover" index = "110" label = "redeliver unacknowledged messages"> + <doc> + This method asks the server to redeliver all unacknowledged messages on a + specified channel. Zero or more messages may be redelivered. This method + replaces the asynchronous Recover. + </doc> + <rule name = "01"> + <doc> + The server MUST set the redelivered flag on all messages that are resent. + </doc> + <doc type = "scenario"> + TODO. + </doc> + </rule> + <chassis name = "server" implement = "MUST" /> + <field name = "requeue" domain = "bit" label = "requeue the message"> + <doc> + If this field is zero, the message will be redelivered to the original + recipient. If this bit is 1, the server will attempt to requeue the message, + potentially then delivering it to an alternative subscriber. + </doc> + </field> + </method> + + <method name = "recover-ok" synchronous = "1" index = "111" label = "confirm recovery"> + <doc> + This method acknowledges a Basic.Recover method. + </doc> + <chassis name = "client" implement = "MUST" /> + </method> + + <method name = "nack" index = "120" label = "reject one or more incoming messages"> + <doc> + This method allows a client to reject one or more incoming messages. It can be + used to interrupt and cancel large incoming messages, or return untreatable + messages to their original queue. + + This method is also used by the server to inform publishers on channels in + confirm mode of unhandled messages. If a publisher receives this method, it + probably needs to republish the offending messages. + </doc> + + <rule name = "01"> + <doc> + The server SHOULD be capable of accepting and processing the Nack method while + sending message content with a Deliver or Get-Ok method. I.e. the server should + read and process incoming methods while sending output frames. To cancel a + partially-send content, the server sends a content body frame of size 1 (i.e. + with no data except the frame-end octet). + </doc> + </rule> + + <rule name = "02"> + <doc> + The server SHOULD interpret this method as meaning that the client is unable to + process the message at this time. + </doc> + <doc type = "scenario"> + TODO. + </doc> + </rule> + + <rule name = "03"> + <doc> + The client MUST NOT use this method as a means of selecting messages to process. + </doc> + <doc type = "scenario"> + TODO. + </doc> + </rule> + + <rule name = "04"> + <doc> + A client publishing messages to a channel in confirm mode SHOULD be capable of accepting + and somehow handling the Nack method. + </doc> + <doc type = "scenario"> + TODO + </doc> + </rule> + + <chassis name = "server" implement = "MUST" /> + <chassis name = "client" implement = "MUST" /> + + <field name = "delivery-tag" domain = "delivery-tag" /> + + <field name = "multiple" domain = "bit" label = "reject multiple messages"> + <doc> + If set to 1, the delivery tag is treated as "up to and + including", so that multiple messages can be rejected + with a single method. If set to zero, the delivery tag + refers to a single message. If the multiple field is 1, and + the delivery tag is zero, this indicates rejection of + all outstanding messages. + </doc> + <rule name = "exists" on-failure = "precondition-failed"> + <doc> + A message MUST not be rejected more than once. The + receiving peer MUST validate that a non-zero delivery-tag + refers to an unacknowledged, delivered message, and + raise a channel exception if this is not the case. + </doc> + <doc type = "scenario"> + TODO. + </doc> + </rule> + </field> + + <field name = "requeue" domain = "bit" label = "requeue the message"> + <doc> + If requeue is true, the server will attempt to requeue the message. If requeue + is false or the requeue attempt fails the messages are discarded or dead-lettered. + Clients receiving the Nack methods should ignore this flag. + </doc> + + <rule name = "01"> + <doc> + The server MUST NOT deliver the message to the same client within the + context of the current channel. The recommended strategy is to attempt to + deliver the message to an alternative consumer, and if that is not possible, + to move the message to a dead-letter queue. The server MAY use more + sophisticated tracking to hold the message on the queue and redeliver it to + the same client at a later stage. + </doc> + <doc type = "scenario"> + TODO. + </doc> + </rule> + </field> + </method> + + </class> + + <!-- == TX =============================================================== --> + + <class name = "tx" handler = "channel" index = "90" label = "work with transactions"> + <doc> + The Tx class allows publish and ack operations to be batched into atomic + units of work. The intention is that all publish and ack requests issued + within a transaction will complete successfully or none of them will. + Servers SHOULD implement atomic transactions at least where all publish + or ack requests affect a single queue. Transactions that cover multiple + queues may be non-atomic, given that queues can be created and destroyed + asynchronously, and such events do not form part of any transaction. + Further, the behaviour of transactions with respect to the immediate and + mandatory flags on Basic.Publish methods is not defined. + </doc> + + <rule name = "not multiple queues"> + <doc> + Applications MUST NOT rely on the atomicity of transactions that + affect more than one queue. + </doc> + </rule> + <rule name = "not immediate"> + <doc> + Applications MUST NOT rely on the behaviour of transactions that + include messages published with the immediate option. + </doc> + </rule> + <rule name = "not mandatory"> + <doc> + Applications MUST NOT rely on the behaviour of transactions that + include messages published with the mandatory option. + </doc> + </rule> + + <doc type = "grammar"> + tx = C:SELECT S:SELECT-OK + / C:COMMIT S:COMMIT-OK + / C:ROLLBACK S:ROLLBACK-OK + </doc> + + <chassis name = "server" implement = "SHOULD" /> + <chassis name = "client" implement = "MAY" /> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "select" synchronous = "1" index = "10" label = "select standard transaction mode"> + <doc> + This method sets the channel to use standard transactions. The client must use this + method at least once on a channel before using the Commit or Rollback methods. + </doc> + <chassis name = "server" implement = "MUST" /> + <response name = "select-ok" /> + </method> + + <method name = "select-ok" synchronous = "1" index = "11" label = "confirm transaction mode"> + <doc> + This method confirms to the client that the channel was successfully set to use + standard transactions. + </doc> + <chassis name = "client" implement = "MUST" /> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "commit" synchronous = "1" index = "20" label = "commit the current transaction"> + <doc> + This method commits all message publications and acknowledgments performed in + the current transaction. A new transaction starts immediately after a commit. + </doc> + <chassis name = "server" implement = "MUST" /> + <response name = "commit-ok" /> + + <rule name = "transacted" on-failure = "precondition-failed"> + <doc> + The client MUST NOT use the Commit method on non-transacted channels. + </doc> + <doc type = "scenario"> + The client opens a channel and then uses Tx.Commit. + </doc> + </rule> + </method> + + <method name = "commit-ok" synchronous = "1" index = "21" label = "confirm a successful commit"> + <doc> + This method confirms to the client that the commit succeeded. Note that if a commit + fails, the server raises a channel exception. + </doc> + <chassis name = "client" implement = "MUST" /> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "rollback" synchronous = "1" index = "30" + label = "abandon the current transaction"> + <doc> + This method abandons all message publications and acknowledgments performed in + the current transaction. A new transaction starts immediately after a rollback. + Note that unacked messages will not be automatically redelivered by rollback; + if that is required an explicit recover call should be issued. + </doc> + <chassis name = "server" implement = "MUST" /> + <response name = "rollback-ok" /> + + <rule name = "transacted" on-failure = "precondition-failed"> + <doc> + The client MUST NOT use the Rollback method on non-transacted channels. + </doc> + <doc type = "scenario"> + The client opens a channel and then uses Tx.Rollback. + </doc> + </rule> + </method> + + <method name = "rollback-ok" synchronous = "1" index = "31" label = "confirm successful rollback"> + <doc> + This method confirms to the client that the rollback succeeded. Note that if an + rollback fails, the server raises a channel exception. + </doc> + <chassis name = "client" implement = "MUST" /> + </method> + </class> + + <!-- == CONFIRM ========================================================== --> + + <class name = "confirm" handler = "channel" index = "85" label = "work with confirms"> + <doc> + The Confirm class allows publishers to put the channel in + confirm mode and subsequently be notified when messages have been + handled by the broker. The intention is that all messages + published on a channel in confirm mode will be acknowledged at + some point. By acknowledging a message the broker assumes + responsibility for it and indicates that it has done something + it deems reasonable with it. + + Unroutable mandatory or immediate messages are acknowledged + right after the Basic.Return method. Messages are acknowledged + when all queues to which the message has been routed + have either delivered the message and received an + acknowledgement (if required), or enqueued the message (and + persisted it if required). + + Published messages are assigned ascending sequence numbers, + starting at 1 with the first Confirm.Select method. The server + confirms messages by sending Basic.Ack methods referring to these + sequence numbers. + </doc> + + <rule name = "all messages acknowledged"> + <doc> + The server MUST acknowledge all messages received after the + channel was put into confirm mode. + </doc> + </rule> + + <rule name = "all queues"> + <doc> + The server MUST acknowledge a message only after it was + properly handled by all the queues it was delivered to. + </doc> + </rule> + + <rule name = "unroutable messages"> + <doc> + The server MUST acknowledge an unroutable mandatory or + immediate message only after it sends the Basic.Return. + </doc> + </rule> + + <rule name = "time guarantees"> + <doc> + No guarantees are made as to how soon a message is + acknowledged. Applications SHOULD NOT make assumptions about + this. + </doc> + </rule> + + <doc type = "grammar"> + confirm = C:SELECT S:SELECT-OK + </doc> + + <chassis name = "server" implement = "SHOULD" /> + <chassis name = "client" implement = "MAY" /> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name="select" synchronous="1" index="10"> + select confirm mode (i.e. enable publisher acknowledgements) + <doc> + This method sets the channel to use publisher acknowledgements. + The client can only use this method on a non-transactional + channel. + </doc> + <chassis name="server" implement="MUST"/> + <response name="select-ok"/> + <field name = "nowait" type = "bit"> + do not send a reply method + <doc> + If set, the server will not respond to the method. The client should + not wait for a reply method. If the server could not complete the + method it will raise a channel or connection exception. + </doc> + </field> + </method> + + <method name="select-ok" synchronous="1" index="11"> + acknowledge confirm mode + <doc> + This method confirms to the client that the channel was successfully + set to use publisher acknowledgements. + </doc> + <chassis name="client" implement="MUST"/> + </method> + </class> + +</amqp> diff --git a/resources/amqp0-9-1.xml b/resources/amqp0-9-1.xml new file mode 100644 index 0000000000000000000000000000000000000000..da785eb3bed370d7c11852fce8ef7fe7f366f2f6 --- /dev/null +++ b/resources/amqp0-9-1.xml @@ -0,0 +1,2843 @@ +<?xml version = "1.0"?> +<!-- + Copyright Notice + ================ + Copyright (c) 2006-2008 Cisco Systems, Credit Suisse, Deutsche Boerse + Systems, Envoy Technologies, Inc., Goldman Sachs, IONA Technologies PLC, + iMatix Corporation, JPMorgan Chase Bank Inc. N.A, Novell, Rabbit + Technologies Ltd., Red Hat, Inc., TWIST Process Innovations Ltd, WS02 + Inc. and 29West Inc. All rights reserved. + + License + ======= + Cisco Systems, Credit Suisse, Deutsche Boerse Systems, Envoy Technologies, + Inc., Goldman Sachs, IONA Technologies PLC, iMatix Corporation, JPMorgan + Chase Bank Inc. N.A, Novell, Rabbit Technologies Ltd., Red Hat, Inc., + TWIST Process Innovations Ltd, WS02, Inc. and 29West Inc. (collectively, + the "Authors") each hereby grants to you a worldwide, perpetual, + royalty-free, nontransferable, nonexclusive license to (i) copy, display, + distribute and implement the Advanced Messaging Queue Protocol ("AMQP") + Specification and (ii) the Licensed Claims that are held by the Authors, + all for the purpose of implementing the Advanced Messaging Queue Protocol + Specification. Your license and any rights under this Agreement will + terminate immediately without notice from any Author if you bring any + claim, suit, demand, or action related to the Advanced Messaging Queue + Protocol Specification against any Author. Upon termination, you shall + destroy all copies of the Advanced Messaging Queue Protocol Specification + in your possession or control. + + As used hereunder, "Licensed Claims" means those claims of a patent or + patent application, throughout the world, excluding design patents and + design registrations, owned or controlled, or that can be sublicensed + without fee and in compliance with the requirements of this Agreement, + by an Author or its affiliates now or at any future time and which would + necessarily be infringed by implementation of the Advanced Messaging + Queue Protocol Specification. A claim is necessarily infringed hereunder + only when it is not possible to avoid infringing it because there is no + plausible non-infringing alternative for implementing the required + portions of the Advanced Messaging Queue Protocol Specification. + Notwithstanding the foregoing, Licensed Claims shall not include any + claims other than as set forth above even if contained in the same patent + as Licensed Claims; or that read solely on any implementations of any + portion of the Advanced Messaging Queue Protocol Specification that are + not required by the Advanced Messaging Queue ProtocolSpecification, or + that, if licensed, would require a payment of royalties by the licensor + to unaffiliated third parties. Moreover, Licensed Claims shall not + include (i) any enabling technologies that may be necessary to make or + use any Licensed Product but are not themselves expressly set forth in + the Advanced Messaging Queue Protocol Specification (e.g., semiconductor + manufacturing technology, compiler technology, object oriented + technology, networking technology, operating system technology, and the + like); or (ii) the implementation of other published standards developed + elsewhere and merely referred to in the body of the Advanced Messaging + Queue Protocol Specification, or (iii) any Licensed Product and any + combinations thereof the purpose or function of which is not required + for compliance with the Advanced Messaging Queue Protocol Specification. + For purposes of this definition, the Advanced Messaging Queue Protocol + Specification shall be deemed to include both architectural and + interconnection requirements essential for interoperability and may also + include supporting source code artifacts where such architectural, + interconnection requirements and source code artifacts are expressly + identified as being required or documentation to achieve compliance with + the Advanced Messaging Queue Protocol Specification. + + As used hereunder, "Licensed Products" means only those specific portions + of products (hardware, software or combinations thereof) that implement + and are compliant with all relevant portions of the Advanced Messaging + Queue Protocol Specification. + + The following disclaimers, which you hereby also acknowledge as to any + use you may make of the Advanced Messaging Queue Protocol Specification: + + THE ADVANCED MESSAGING QUEUE PROTOCOL SPECIFICATION IS PROVIDED "AS IS," + AND THE AUTHORS MAKE NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR + IMPLIED, INCLUDING, BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, OR TITLE; THAT THE + CONTENTS OF THE ADVANCED MESSAGING QUEUE PROTOCOL SPECIFICATION ARE + SUITABLE FOR ANY PURPOSE; NOR THAT THE IMPLEMENTATION OF THE ADVANCED + MESSAGING QUEUE PROTOCOL SPECIFICATION WILL NOT INFRINGE ANY THIRD PARTY + PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS. + + THE AUTHORS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL, + INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF OR RELATING TO ANY + USE, IMPLEMENTATION OR DISTRIBUTION OF THE ADVANCED MESSAGING QUEUE + PROTOCOL SPECIFICATION. + + The name and trademarks of the Authors may NOT be used in any manner, + including advertising or publicity pertaining to the Advanced Messaging + Queue Protocol Specification or its contents without specific, written + prior permission. Title to copyright in the Advanced Messaging Queue + Protocol Specification will at all times remain with the Authors. + + No other rights are granted by implication, estoppel or otherwise. + + Upon termination of your license or rights under this Agreement, you + shall destroy all copies of the Advanced Messaging Queue Protocol + Specification in your possession or control. + + Trademarks + ========== + JPMorgan, JPMorgan Chase, Chase, the JPMorgan Chase logo and the + Octagon Symbol are trademarks of JPMorgan Chase & Co. + + IMATIX and the iMatix logo are trademarks of iMatix Corporation sprl. + + IONA, IONA Technologies, and the IONA logos are trademarks of IONA + Technologies PLC and/or its subsidiaries. + + LINUX is a trademark of Linus Torvalds. RED HAT and JBOSS are registered + trademarks of Red Hat, Inc. in the US and other countries. + + Java, all Java-based trademarks and OpenOffice.org are trademarks of + Sun Microsystems, Inc. in the United States, other countries, or both. + + Other company, product, or service names may be trademarks or service + marks of others. + + Links to full AMQP specification: + ================================= + http://www.amqp.org +--> + +<!-- + <!DOCTYPE amqp SYSTEM "amqp.dtd"> +--> + +<!-- XML Notes + + We use entities to indicate repetition; attributes to indicate properties. + + We use the 'name' attribute as an identifier, usually within the context + of the surrounding entities. + + We use spaces to seperate words in names, so that we can print names in + their natural form depending on the context - underlines for source code, + hyphens for written text, etc. + + We do not enforce any particular validation mechanism but we support all + mechanisms. The protocol definition conforms to a formal grammar that is + published seperately in several technologies. + + --> + +<amqp major = "0" minor = "9" revision = "1" + port = "5672" comment = "AMQ Protocol version 0-9-1"> + <!-- + ====================================================== + == CONSTANTS + ====================================================== + --> + <!-- Frame types --> + <constant name = "frame-method" value = "1" /> + <constant name = "frame-header" value = "2" /> + <constant name = "frame-body" value = "3" /> + <constant name = "frame-heartbeat" value = "8" /> + + <!-- Protocol constants --> + <constant name = "frame-min-size" value = "4096" /> + <constant name = "frame-end" value = "206" /> + + <!-- Reply codes --> + <constant name = "reply-success" value = "200"> + <doc> + Indicates that the method completed successfully. This reply code is + reserved for future use - the current protocol design does not use positive + confirmation and reply codes are sent only in case of an error. + </doc> + </constant> + + <constant name = "content-too-large" value = "311" class = "soft-error"> + <doc> + The client attempted to transfer content larger than the server could accept + at the present time. The client may retry at a later time. + </doc> + </constant> + + <constant name = "no-consumers" value = "313" class = "soft-error"> + <doc> + When the exchange cannot deliver to a consumer when the immediate flag is + set. As a result of pending data on the queue or the absence of any + consumers of the queue. + </doc> + </constant> + + <constant name = "connection-forced" value = "320" class = "hard-error"> + <doc> + An operator intervened to close the connection for some reason. The client + may retry at some later date. + </doc> + </constant> + + <constant name = "invalid-path" value = "402" class = "hard-error"> + <doc> + The client tried to work with an unknown virtual host. + </doc> + </constant> + + <constant name = "access-refused" value = "403" class = "soft-error"> + <doc> + The client attempted to work with a server entity to which it has no + access due to security settings. + </doc> + </constant> + + <constant name = "not-found" value = "404" class = "soft-error"> + <doc> + The client attempted to work with a server entity that does not exist. + </doc> + </constant> + + <constant name = "resource-locked" value = "405" class = "soft-error"> + <doc> + The client attempted to work with a server entity to which it has no + access because another client is working with it. + </doc> + </constant> + + <constant name = "precondition-failed" value = "406" class = "soft-error"> + <doc> + The client requested a method that was not allowed because some precondition + failed. + </doc> + </constant> + + <constant name = "frame-error" value = "501" class = "hard-error"> + <doc> + The sender sent a malformed frame that the recipient could not decode. + This strongly implies a programming error in the sending peer. + </doc> + </constant> + + <constant name = "syntax-error" value = "502" class = "hard-error"> + <doc> + The sender sent a frame that contained illegal values for one or more + fields. This strongly implies a programming error in the sending peer. + </doc> + </constant> + + <constant name = "command-invalid" value = "503" class = "hard-error"> + <doc> + The client sent an invalid sequence of frames, attempting to perform an + operation that was considered invalid by the server. This usually implies + a programming error in the client. + </doc> + </constant> + + <constant name = "channel-error" value = "504" class = "hard-error"> + <doc> + The client attempted to work with a channel that had not been correctly + opened. This most likely indicates a fault in the client layer. + </doc> + </constant> + + <constant name = "unexpected-frame" value = "505" class = "hard-error"> + <doc> + The peer sent a frame that was not expected, usually in the context of + a content header and body. This strongly indicates a fault in the peer's + content processing. + </doc> + </constant> + + <constant name = "resource-error" value = "506" class = "hard-error"> + <doc> + The server could not complete the method because it lacked sufficient + resources. This may be due to the client creating too many of some type + of entity. + </doc> + </constant> + + <constant name = "not-allowed" value = "530" class = "hard-error"> + <doc> + The client tried to work with some entity in a manner that is prohibited + by the server, due to security settings or by some other criteria. + </doc> + </constant> + + <constant name = "not-implemented" value = "540" class = "hard-error"> + <doc> + The client tried to use functionality that is not implemented in the + server. + </doc> + </constant> + + <constant name = "internal-error" value = "541" class = "hard-error"> + <doc> + The server could not complete the method because of an internal error. + The server may require intervention by an operator in order to resume + normal operations. + </doc> + </constant> + + <!-- + ====================================================== + == DOMAIN TYPES + ====================================================== + --> + + <domain name = "class-id" type = "short" /> + + <domain name = "consumer-tag" type = "shortstr" label = "consumer tag"> + <doc> + Identifier for the consumer, valid within the current channel. + </doc> + </domain> + + <domain name = "delivery-tag" type = "longlong" label = "server-assigned delivery tag"> + <doc> + The server-assigned and channel-specific delivery tag + </doc> + <rule name = "channel-local"> + <doc> + The delivery tag is valid only within the channel from which the message was + received. I.e. a client MUST NOT receive a message on one channel and then + acknowledge it on another. + </doc> + </rule> + <rule name = "non-zero"> + <doc> + The server MUST NOT use a zero value for delivery tags. Zero is reserved + for client use, meaning "all messages so far received". + </doc> + </rule> + </domain> + + <domain name = "exchange-name" type = "shortstr" label = "exchange name"> + <doc> + The exchange name is a client-selected string that identifies the exchange for + publish methods. + </doc> + <assert check = "length" value = "127" /> + <assert check = "regexp" value = "^[a-zA-Z0-9-_.:]*$" /> + </domain> + + <domain name = "method-id" type = "short" /> + + <domain name = "no-ack" type = "bit" label = "no acknowledgement needed"> + <doc> + If this field is set the server does not expect acknowledgements for + messages. That is, when a message is delivered to the client the server + assumes the delivery will succeed and immediately dequeues it. This + functionality may increase performance but at the cost of reliability. + Messages can get lost if a client dies before they are delivered to the + application. + </doc> + </domain> + + <domain name = "no-local" type = "bit" label = "do not deliver own messages"> + <doc> + If the no-local field is set the server will not send messages to the connection that + published them. + </doc> + </domain> + + <domain name = "no-wait" type = "bit" label = "do not send reply method"> + <doc> + If set, the server will not respond to the method. The client should not wait + for a reply method. If the server could not complete the method it will raise a + channel or connection exception. + </doc> + </domain> + + <domain name = "path" type = "shortstr"> + <doc> + Unconstrained. + </doc> + <assert check = "notnull" /> + <assert check = "length" value = "127" /> + </domain> + + <domain name = "peer-properties" type = "table"> + <doc> + This table provides a set of peer properties, used for identification, debugging, + and general information. + </doc> + </domain> + + <domain name = "queue-name" type = "shortstr" label = "queue name"> + <doc> + The queue name identifies the queue within the vhost. In methods where the queue + name may be blank, and that has no specific significance, this refers to the + 'current' queue for the channel, meaning the last queue that the client declared + on the channel. If the client did not declare a queue, and the method needs a + queue name, this will result in a 502 (syntax error) channel exception. + </doc> + <assert check = "length" value = "127" /> + <assert check = "regexp" value = "^[a-zA-Z0-9-_.:]*$" /> + </domain> + + <domain name = "redelivered" type = "bit" label = "message is being redelivered"> + <doc> + This indicates that the message has been previously delivered to this or + another client. + </doc> + <rule name = "implementation"> + <doc> + The server SHOULD try to signal redelivered messages when it can. When + redelivering a message that was not successfully acknowledged, the server + SHOULD deliver it to the original client if possible. + </doc> + <doc type = "scenario"> + Declare a shared queue and publish a message to the queue. Consume the + message using explicit acknowledgements, but do not acknowledge the + message. Close the connection, reconnect, and consume from the queue + again. The message should arrive with the redelivered flag set. + </doc> + </rule> + <rule name = "hinting"> + <doc> + The client MUST NOT rely on the redelivered field but should take it as a + hint that the message may already have been processed. A fully robust + client must be able to track duplicate received messages on non-transacted, + and locally-transacted channels. + </doc> + </rule> + </domain> + + <domain name = "message-count" type = "long" label = "number of messages in queue"> + <doc> + The number of messages in the queue, which will be zero for newly-declared + queues. This is the number of messages present in the queue, and committed + if the channel on which they were published is transacted, that are not + waiting acknowledgement. + </doc> + </domain> + + <domain name = "reply-code" type = "short" label = "reply code from server"> + <doc> + The reply code. The AMQ reply codes are defined as constants at the start + of this formal specification. + </doc> + <assert check = "notnull" /> + </domain> + + <domain name = "reply-text" type = "shortstr" label = "localised reply text"> + <doc> + The localised reply text. This text can be logged as an aid to resolving + issues. + </doc> + <assert check = "notnull" /> + </domain> + + <!-- Elementary domains --> + <domain name = "bit" type = "bit" label = "single bit" /> + <domain name = "octet" type = "octet" label = "single octet" /> + <domain name = "short" type = "short" label = "16-bit integer" /> + <domain name = "long" type = "long" label = "32-bit integer" /> + <domain name = "longlong" type = "longlong" label = "64-bit integer" /> + <domain name = "shortstr" type = "shortstr" label = "short string" /> + <domain name = "longstr" type = "longstr" label = "long string" /> + <domain name = "timestamp" type = "timestamp" label = "64-bit timestamp" /> + <domain name = "table" type = "table" label = "field table" /> + + <!-- == CONNECTION ======================================================= --> + + <class name = "connection" handler = "connection" index = "10" label = "work with socket connections"> + <doc> + The connection class provides methods for a client to establish a network connection to + a server, and for both peers to operate the connection thereafter. + </doc> + + <doc type = "grammar"> + connection = open-connection *use-connection close-connection + open-connection = C:protocol-header + S:START C:START-OK + *challenge + S:TUNE C:TUNE-OK + C:OPEN S:OPEN-OK + challenge = S:SECURE C:SECURE-OK + use-connection = *channel + close-connection = C:CLOSE S:CLOSE-OK + / S:CLOSE C:CLOSE-OK + </doc> + + <chassis name = "server" implement = "MUST" /> + <chassis name = "client" implement = "MUST" /> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "start" synchronous = "1" index = "10" label = "start connection negotiation"> + <doc> + This method starts the connection negotiation process by telling the client the + protocol version that the server proposes, along with a list of security mechanisms + which the client can use for authentication. + </doc> + + <rule name = "protocol-name"> + <doc> + If the server cannot support the protocol specified in the protocol header, + it MUST respond with a valid protocol header and then close the socket + connection. + </doc> + <doc type = "scenario"> + The client sends a protocol header containing an invalid protocol name. + The server MUST respond by sending a valid protocol header and then closing + the connection. + </doc> + </rule> + <rule name = "server-support"> + <doc> + The server MUST provide a protocol version that is lower than or equal to + that requested by the client in the protocol header. + </doc> + <doc type = "scenario"> + The client requests a protocol version that is higher than any valid + implementation, e.g. 2.0. The server must respond with a protocol header + indicating its supported protocol version, e.g. 1.0. + </doc> + </rule> + <rule name = "client-support"> + <doc> + If the client cannot handle the protocol version suggested by the server + it MUST close the socket connection without sending any further data. + </doc> + <doc type = "scenario"> + The server sends a protocol version that is lower than any valid + implementation, e.g. 0.1. The client must respond by closing the + connection without sending any further data. + </doc> + </rule> + + <chassis name = "client" implement = "MUST" /> + <response name = "start-ok" /> + + <field name = "version-major" domain = "octet" label = "protocol major version"> + <doc> + The major version number can take any value from 0 to 99 as defined in the + AMQP specification. + </doc> + </field> + + <field name = "version-minor" domain = "octet" label = "protocol minor version"> + <doc> + The minor version number can take any value from 0 to 99 as defined in the + AMQP specification. + </doc> + </field> + + <field name = "server-properties" domain = "peer-properties" label = "server properties"> + <rule name = "required-fields"> + <doc> + The properties SHOULD contain at least these fields: "host", specifying the + server host name or address, "product", giving the name of the server product, + "version", giving the name of the server version, "platform", giving the name + of the operating system, "copyright", if appropriate, and "information", giving + other general information. + </doc> + <doc type = "scenario"> + Client connects to server and inspects the server properties. It checks for + the presence of the required fields. + </doc> + </rule> + </field> + + <field name = "mechanisms" domain = "longstr" label = "available security mechanisms"> + <doc> + A list of the security mechanisms that the server supports, delimited by spaces. + </doc> + <assert check = "notnull" /> + </field> + + <field name = "locales" domain = "longstr" label = "available message locales"> + <doc> + A list of the message locales that the server supports, delimited by spaces. The + locale defines the language in which the server will send reply texts. + </doc> + <rule name = "required-support"> + <doc> + The server MUST support at least the en_US locale. + </doc> + <doc type = "scenario"> + Client connects to server and inspects the locales field. It checks for + the presence of the required locale(s). + </doc> + </rule> + <assert check = "notnull" /> + </field> + </method> + + <method name = "start-ok" synchronous = "1" index = "11" + label = "select security mechanism and locale"> + <doc> + This method selects a SASL security mechanism. + </doc> + + <chassis name = "server" implement = "MUST" /> + + <field name = "client-properties" domain = "peer-properties" label = "client properties"> + <rule name = "required-fields"> + <!-- This rule is not testable from the client side --> + <doc> + The properties SHOULD contain at least these fields: "product", giving the name + of the client product, "version", giving the name of the client version, "platform", + giving the name of the operating system, "copyright", if appropriate, and + "information", giving other general information. + </doc> + </rule> + </field> + + <field name = "mechanism" domain = "shortstr" label = "selected security mechanism"> + <doc> + A single security mechanisms selected by the client, which must be one of those + specified by the server. + </doc> + <rule name = "security"> + <doc> + The client SHOULD authenticate using the highest-level security profile it + can handle from the list provided by the server. + </doc> + </rule> + <rule name = "validity"> + <doc> + If the mechanism field does not contain one of the security mechanisms + proposed by the server in the Start method, the server MUST close the + connection without sending any further data. + </doc> + <doc type = "scenario"> + Client connects to server and sends an invalid security mechanism. The + server must respond by closing the connection (a socket close, with no + connection close negotiation). + </doc> + </rule> + <assert check = "notnull" /> + </field> + + <field name = "response" domain = "longstr" label = "security response data"> + <doc> + A block of opaque data passed to the security mechanism. The contents of this + data are defined by the SASL security mechanism. + </doc> + <assert check = "notnull" /> + </field> + + <field name = "locale" domain = "shortstr" label = "selected message locale"> + <doc> + A single message locale selected by the client, which must be one of those + specified by the server. + </doc> + <assert check = "notnull" /> + </field> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "secure" synchronous = "1" index = "20" label = "security mechanism challenge"> + <doc> + The SASL protocol works by exchanging challenges and responses until both peers have + received sufficient information to authenticate each other. This method challenges + the client to provide more information. + </doc> + + <chassis name = "client" implement = "MUST" /> + <response name = "secure-ok" /> + + <field name = "challenge" domain = "longstr" label = "security challenge data"> + <doc> + Challenge information, a block of opaque binary data passed to the security + mechanism. + </doc> + </field> + </method> + + <method name = "secure-ok" synchronous = "1" index = "21" label = "security mechanism response"> + <doc> + This method attempts to authenticate, passing a block of SASL data for the security + mechanism at the server side. + </doc> + + <chassis name = "server" implement = "MUST" /> + + <field name = "response" domain = "longstr" label = "security response data"> + <doc> + A block of opaque data passed to the security mechanism. The contents of this + data are defined by the SASL security mechanism. + </doc> + <assert check = "notnull" /> + </field> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "tune" synchronous = "1" index = "30" + label = "propose connection tuning parameters"> + <doc> + This method proposes a set of connection configuration values to the client. The + client can accept and/or adjust these. + </doc> + + <chassis name = "client" implement = "MUST" /> + + <response name = "tune-ok" /> + + <field name = "channel-max" domain = "short" label = "proposed maximum channels"> + <doc> + Specifies highest channel number that the server permits. Usable channel numbers + are in the range 1..channel-max. Zero indicates no specified limit. + </doc> + </field> + + <field name = "frame-max" domain = "long" label = "proposed maximum frame size"> + <doc> + The largest frame size that the server proposes for the connection, including + frame header and end-byte. The client can negotiate a lower value. Zero means + that the server does not impose any specific limit but may reject very large + frames if it cannot allocate resources for them. + </doc> + <rule name = "minimum"> + <doc> + Until the frame-max has been negotiated, both peers MUST accept frames of up + to frame-min-size octets large, and the minimum negotiated value for frame-max + is also frame-min-size. + </doc> + <doc type = "scenario"> + Client connects to server and sends a large properties field, creating a frame + of frame-min-size octets. The server must accept this frame. + </doc> + </rule> + </field> + + <field name = "heartbeat" domain = "short" label = "desired heartbeat delay"> + <doc> + The delay, in seconds, of the connection heartbeat that the server wants. + Zero means the server does not want a heartbeat. + </doc> + </field> + </method> + + <method name = "tune-ok" synchronous = "1" index = "31" + label = "negotiate connection tuning parameters"> + <doc> + This method sends the client's connection tuning parameters to the server. + Certain fields are negotiated, others provide capability information. + </doc> + + <chassis name = "server" implement = "MUST" /> + + <field name = "channel-max" domain = "short" label = "negotiated maximum channels"> + <doc> + The maximum total number of channels that the client will use per connection. + </doc> + <rule name = "upper-limit"> + <doc> + If the client specifies a channel max that is higher than the value provided + by the server, the server MUST close the connection without attempting a + negotiated close. The server may report the error in some fashion to assist + implementors. + </doc> + </rule> + <assert check = "notnull" /> + <assert check = "le" method = "tune" field = "channel-max" /> + </field> + + <field name = "frame-max" domain = "long" label = "negotiated maximum frame size"> + <doc> + The largest frame size that the client and server will use for the connection. + Zero means that the client does not impose any specific limit but may reject + very large frames if it cannot allocate resources for them. Note that the + frame-max limit applies principally to content frames, where large contents can + be broken into frames of arbitrary size. + </doc> + <rule name = "minimum"> + <doc> + Until the frame-max has been negotiated, both peers MUST accept frames of up + to frame-min-size octets large, and the minimum negotiated value for frame-max + is also frame-min-size. + </doc> + </rule> + <rule name = "upper-limit"> + <doc> + If the client specifies a frame max that is higher than the value provided + by the server, the server MUST close the connection without attempting a + negotiated close. The server may report the error in some fashion to assist + implementors. + </doc> + </rule> + </field> + + <field name = "heartbeat" domain = "short" label = "desired heartbeat delay"> + <doc> + The delay, in seconds, of the connection heartbeat that the client wants. Zero + means the client does not want a heartbeat. + </doc> + </field> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "open" synchronous = "1" index = "40" label = "open connection to virtual host"> + <doc> + This method opens a connection to a virtual host, which is a collection of + resources, and acts to separate multiple application domains within a server. + The server may apply arbitrary limits per virtual host, such as the number + of each type of entity that may be used, per connection and/or in total. + </doc> + + <chassis name = "server" implement = "MUST" /> + <response name = "open-ok" /> + + <field name = "virtual-host" domain = "path" label = "virtual host name"> + <doc> + The name of the virtual host to work with. + </doc> + <rule name = "separation"> + <doc> + If the server supports multiple virtual hosts, it MUST enforce a full + separation of exchanges, queues, and all associated entities per virtual + host. An application, connected to a specific virtual host, MUST NOT be able + to access resources of another virtual host. + </doc> + </rule> + <rule name = "security"> + <doc> + The server SHOULD verify that the client has permission to access the + specified virtual host. + </doc> + </rule> + </field> + <!-- Deprecated: "capabilities", must be zero --> + <field name = "reserved-1" type = "shortstr" reserved = "1" /> + <!-- Deprecated: "insist", must be zero --> + <field name = "reserved-2" type = "bit" reserved = "1" /> + </method> + + <method name = "open-ok" synchronous = "1" index = "41" label = "signal that connection is ready"> + <doc> + This method signals to the client that the connection is ready for use. + </doc> + <chassis name = "client" implement = "MUST" /> + <!-- Deprecated: "known-hosts", must be zero --> + <field name = "reserved-1" type = "shortstr" reserved = "1" /> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "close" synchronous = "1" index = "50" label = "request a connection close"> + <doc> + This method indicates that the sender wants to close the connection. This may be + due to internal conditions (e.g. a forced shut-down) or due to an error handling + a specific method, i.e. an exception. When a close is due to an exception, the + sender provides the class and method id of the method which caused the exception. + </doc> + <rule name = "stability"> + <doc> + After sending this method, any received methods except Close and Close-OK MUST + be discarded. The response to receiving a Close after sending Close must be to + send Close-Ok. + </doc> + </rule> + + <chassis name = "client" implement = "MUST" /> + <chassis name = "server" implement = "MUST" /> + <response name = "close-ok" /> + + <field name = "reply-code" domain = "reply-code" /> + <field name = "reply-text" domain = "reply-text" /> + + <field name = "class-id" domain = "class-id" label = "failing method class"> + <doc> + When the close is provoked by a method exception, this is the class of the + method. + </doc> + </field> + + <field name = "method-id" domain = "method-id" label = "failing method ID"> + <doc> + When the close is provoked by a method exception, this is the ID of the method. + </doc> + </field> + </method> + + <method name = "close-ok" synchronous = "1" index = "51" label = "confirm a connection close"> + <doc> + This method confirms a Connection.Close method and tells the recipient that it is + safe to release resources for the connection and close the socket. + </doc> + <rule name = "reporting"> + <doc> + A peer that detects a socket closure without having received a Close-Ok + handshake method SHOULD log the error. + </doc> + </rule> + <chassis name = "client" implement = "MUST" /> + <chassis name = "server" implement = "MUST" /> + </method> + </class> + + <!-- == CHANNEL ========================================================== --> + + <class name = "channel" handler = "channel" index = "20" label = "work with channels"> + <doc> + The channel class provides methods for a client to establish a channel to a + server and for both peers to operate the channel thereafter. + </doc> + + <doc type = "grammar"> + channel = open-channel *use-channel close-channel + open-channel = C:OPEN S:OPEN-OK + use-channel = C:FLOW S:FLOW-OK + / S:FLOW C:FLOW-OK + / functional-class + close-channel = C:CLOSE S:CLOSE-OK + / S:CLOSE C:CLOSE-OK + </doc> + + <chassis name = "server" implement = "MUST" /> + <chassis name = "client" implement = "MUST" /> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "open" synchronous = "1" index = "10" label = "open a channel for use"> + <doc> + This method opens a channel to the server. + </doc> + <rule name = "state" on-failure = "channel-error"> + <doc> + The client MUST NOT use this method on an already-opened channel. + </doc> + <doc type = "scenario"> + Client opens a channel and then reopens the same channel. + </doc> + </rule> + <chassis name = "server" implement = "MUST" /> + <response name = "open-ok" /> + <!-- Deprecated: "out-of-band", must be zero --> + <field name = "reserved-1" type = "shortstr" reserved = "1" /> + </method> + + <method name = "open-ok" synchronous = "1" index = "11" label = "signal that the channel is ready"> + <doc> + This method signals to the client that the channel is ready for use. + </doc> + <chassis name = "client" implement = "MUST" /> + <!-- Deprecated: "channel-id", must be zero --> + <field name = "reserved-1" type = "longstr" reserved = "1" /> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "flow" synchronous = "1" index = "20" label = "enable/disable flow from peer"> + <doc> + This method asks the peer to pause or restart the flow of content data sent by + a consumer. This is a simple flow-control mechanism that a peer can use to avoid + overflowing its queues or otherwise finding itself receiving more messages than + it can process. Note that this method is not intended for window control. It does + not affect contents returned by Basic.Get-Ok methods. + </doc> + + <rule name = "initial-state"> + <doc> + When a new channel is opened, it is active (flow is active). Some applications + assume that channels are inactive until started. To emulate this behaviour a + client MAY open the channel, then pause it. + </doc> + </rule> + + <rule name = "bidirectional"> + <doc> + When sending content frames, a peer SHOULD monitor the channel for incoming + methods and respond to a Channel.Flow as rapidly as possible. + </doc> + </rule> + + <rule name = "throttling"> + <doc> + A peer MAY use the Channel.Flow method to throttle incoming content data for + internal reasons, for example, when exchanging data over a slower connection. + </doc> + </rule> + + <rule name = "expected-behaviour"> + <doc> + The peer that requests a Channel.Flow method MAY disconnect and/or ban a peer + that does not respect the request. This is to prevent badly-behaved clients + from overwhelming a server. + </doc> + </rule> + + <chassis name = "server" implement = "MUST" /> + <chassis name = "client" implement = "MUST" /> + + <response name = "flow-ok" /> + + <field name = "active" domain = "bit" label = "start/stop content frames"> + <doc> + If 1, the peer starts sending content frames. If 0, the peer stops sending + content frames. + </doc> + </field> + </method> + + <method name = "flow-ok" index = "21" label = "confirm a flow method"> + <doc> + Confirms to the peer that a flow command was received and processed. + </doc> + <chassis name = "server" implement = "MUST" /> + <chassis name = "client" implement = "MUST" /> + <field name = "active" domain = "bit" label = "current flow setting"> + <doc> + Confirms the setting of the processed flow method: 1 means the peer will start + sending or continue to send content frames; 0 means it will not. + </doc> + </field> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "close" synchronous = "1" index = "40" label = "request a channel close"> + <doc> + This method indicates that the sender wants to close the channel. This may be due to + internal conditions (e.g. a forced shut-down) or due to an error handling a specific + method, i.e. an exception. When a close is due to an exception, the sender provides + the class and method id of the method which caused the exception. + </doc> + <rule name = "stability"> + <doc> + After sending this method, any received methods except Close and Close-OK MUST + be discarded. The response to receiving a Close after sending Close must be to + send Close-Ok. + </doc> + </rule> + + <chassis name = "client" implement = "MUST" /> + <chassis name = "server" implement = "MUST" /> + <response name = "close-ok" /> + + <field name = "reply-code" domain = "reply-code" /> + <field name = "reply-text" domain = "reply-text" /> + + <field name = "class-id" domain = "class-id" label = "failing method class"> + <doc> + When the close is provoked by a method exception, this is the class of the + method. + </doc> + </field> + + <field name = "method-id" domain = "method-id" label = "failing method ID"> + <doc> + When the close is provoked by a method exception, this is the ID of the method. + </doc> + </field> + </method> + + <method name = "close-ok" synchronous = "1" index = "41" label = "confirm a channel close"> + <doc> + This method confirms a Channel.Close method and tells the recipient that it is safe + to release resources for the channel. + </doc> + <rule name = "reporting"> + <doc> + A peer that detects a socket closure without having received a Channel.Close-Ok + handshake method SHOULD log the error. + </doc> + </rule> + <chassis name = "client" implement = "MUST" /> + <chassis name = "server" implement = "MUST" /> + </method> + </class> + + <!-- == EXCHANGE ========================================================= --> + + <class name = "exchange" handler = "channel" index = "40" label = "work with exchanges"> + <doc> + Exchanges match and distribute messages across queues. Exchanges can be configured in + the server or declared at runtime. + </doc> + + <doc type = "grammar"> + exchange = C:DECLARE S:DECLARE-OK + / C:DELETE S:DELETE-OK + </doc> + + <chassis name = "server" implement = "MUST" /> + <chassis name = "client" implement = "MUST" /> + + <rule name = "required-types"> + <doc> + The server MUST implement these standard exchange types: fanout, direct. + </doc> + <doc type = "scenario"> + Client attempts to declare an exchange with each of these standard types. + </doc> + </rule> + <rule name = "recommended-types"> + <doc> + The server SHOULD implement these standard exchange types: topic, headers. + </doc> + <doc type = "scenario"> + Client attempts to declare an exchange with each of these standard types. + </doc> + </rule> + <rule name = "required-instances"> + <doc> + The server MUST, in each virtual host, pre-declare an exchange instance + for each standard exchange type that it implements, where the name of the + exchange instance, if defined, is "amq." followed by the exchange type name. + </doc> + <doc> + The server MUST, in each virtual host, pre-declare at least two direct + exchange instances: one named "amq.direct", the other with no public name + that serves as a default exchange for Publish methods. + </doc> + <doc type = "scenario"> + Client declares a temporary queue and attempts to bind to each required + exchange instance ("amq.fanout", "amq.direct", "amq.topic", and "amq.headers" + if those types are defined). + </doc> + </rule> + <rule name = "default-exchange"> + <doc> + The server MUST pre-declare a direct exchange with no public name to act as + the default exchange for content Publish methods and for default queue bindings. + </doc> + <doc type = "scenario"> + Client checks that the default exchange is active by specifying a queue + binding with no exchange name, and publishing a message with a suitable + routing key but without specifying the exchange name, then ensuring that + the message arrives in the queue correctly. + </doc> + </rule> + <rule name = "default-access"> + <doc> + The server MUST NOT allow clients to access the default exchange except + by specifying an empty exchange name in the Queue.Bind and content Publish + methods. + </doc> + </rule> + <rule name = "extensions"> + <doc> + The server MAY implement other exchange types as wanted. + </doc> + </rule> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "declare" synchronous = "1" index = "10" label = "verify exchange exists, create if needed"> + <doc> + This method creates an exchange if it does not already exist, and if the exchange + exists, verifies that it is of the correct and expected class. + </doc> + <rule name = "minimum"> + <doc> + The server SHOULD support a minimum of 16 exchanges per virtual host and + ideally, impose no limit except as defined by available resources. + </doc> + <doc type = "scenario"> + The client declares as many exchanges as it can until the server reports + an error; the number of exchanges successfully declared must be at least + sixteen. + </doc> + </rule> + + <chassis name = "server" implement = "MUST" /> + <response name = "declare-ok" /> + + <!-- Deprecated: "ticket", must be zero --> + <field name = "reserved-1" type = "short" reserved = "1" /> + + <field name = "exchange" domain = "exchange-name"> + <rule name = "reserved" on-failure = "access-refused"> + <doc> + Exchange names starting with "amq." are reserved for pre-declared and + standardised exchanges. The client MAY declare an exchange starting with + "amq." if the passive option is set, or the exchange already exists. + </doc> + <doc type = "scenario"> + The client attempts to declare a non-existing exchange starting with + "amq." and with the passive option set to zero. + </doc> + </rule> + <rule name = "syntax" on-failure = "precondition-failed"> + <doc> + The exchange name consists of a non-empty sequence of these characters: + letters, digits, hyphen, underscore, period, or colon. + </doc> + <doc type = "scenario"> + The client attempts to declare an exchange with an illegal name. + </doc> + </rule> + <assert check = "notnull" /> + </field> + + <field name = "type" domain = "shortstr" label = "exchange type"> + <doc> + Each exchange belongs to one of a set of exchange types implemented by the + server. The exchange types define the functionality of the exchange - i.e. how + messages are routed through it. It is not valid or meaningful to attempt to + change the type of an existing exchange. + </doc> + <rule name = "typed" on-failure = "not-allowed"> + <doc> + Exchanges cannot be redeclared with different types. The client MUST not + attempt to redeclare an existing exchange with a different type than used + in the original Exchange.Declare method. + </doc> + <doc type = "scenario"> + TODO. + </doc> + </rule> + <rule name = "support" on-failure = "command-invalid"> + <doc> + The client MUST NOT attempt to declare an exchange with a type that the + server does not support. + </doc> + <doc type = "scenario"> + TODO. + </doc> + </rule> + </field> + + <field name = "passive" domain = "bit" label = "do not create exchange"> + <doc> + If set, the server will reply with Declare-Ok if the exchange already + exists with the same name, and raise an error if not. The client can + use this to check whether an exchange exists without modifying the + server state. When set, all other method fields except name and no-wait + are ignored. A declare with both passive and no-wait has no effect. + Arguments are compared for semantic equivalence. + </doc> + <rule name = "not-found"> + <doc> + If set, and the exchange does not already exist, the server MUST + raise a channel exception with reply code 404 (not found). + </doc> + <doc type = "scenario"> + TODO. + </doc> + </rule> + <rule name = "equivalent"> + <doc> + If not set and the exchange exists, the server MUST check that the + existing exchange has the same values for type, durable, and arguments + fields. The server MUST respond with Declare-Ok if the requested + exchange matches these fields, and MUST raise a channel exception if + not. + </doc> + <doc type = "scenario"> + TODO. + </doc> + </rule> + </field> + + <field name = "durable" domain = "bit" label = "request a durable exchange"> + <doc> + If set when creating a new exchange, the exchange will be marked as durable. + Durable exchanges remain active when a server restarts. Non-durable exchanges + (transient exchanges) are purged if/when a server restarts. + </doc> + <rule name = "support"> + <doc> + The server MUST support both durable and transient exchanges. + </doc> + <doc type = "scenario"> + TODO. + </doc> + </rule> + </field> + + <!-- Deprecated: "auto-delete", must be zero --> + <field name = "reserved-2" type = "bit" reserved = "1" /> + <!-- Deprecated: "internal", must be zero --> + <field name = "reserved-3" type = "bit" reserved = "1" /> + <field name = "no-wait" domain = "no-wait" /> + + <field name = "arguments" domain = "table" label = "arguments for declaration"> + <doc> + A set of arguments for the declaration. The syntax and semantics of these + arguments depends on the server implementation. + </doc> + </field> + </method> + + <method name = "declare-ok" synchronous = "1" index = "11" label = "confirm exchange declaration"> + <doc> + This method confirms a Declare method and confirms the name of the exchange, + essential for automatically-named exchanges. + </doc> + <chassis name = "client" implement = "MUST" /> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "delete" synchronous = "1" index = "20" label = "delete an exchange"> + <doc> + This method deletes an exchange. When an exchange is deleted all queue bindings on + the exchange are cancelled. + </doc> + + <chassis name = "server" implement = "MUST" /> + <response name = "delete-ok" /> + + <!-- Deprecated: "ticket", must be zero --> + <field name = "reserved-1" type = "short" reserved = "1" /> + + <field name = "exchange" domain = "exchange-name"> + <rule name = "exists" on-failure = "not-found"> + <doc> + The client MUST NOT attempt to delete an exchange that does not exist. + </doc> + </rule> + <assert check = "notnull" /> + </field> + + <field name = "if-unused" domain = "bit" label = "delete only if unused"> + <doc> + If set, the server will only delete the exchange if it has no queue bindings. If + the exchange has queue bindings the server does not delete it but raises a + channel exception instead. + </doc> + <rule name = "in-use" on-failure = "precondition-failed"> + <doc> + The server MUST NOT delete an exchange that has bindings on it, if the if-unused + field is true. + </doc> + <doc type = "scenario"> + The client declares an exchange, binds a queue to it, then tries to delete it + setting if-unused to true. + </doc> + </rule> + </field> + + <field name = "no-wait" domain = "no-wait" /> + </method> + + <method name = "delete-ok" synchronous = "1" index = "21" + label = "confirm deletion of an exchange"> + <doc>This method confirms the deletion of an exchange.</doc> + <chassis name = "client" implement = "MUST" /> + </method> + </class> + + <!-- == QUEUE ============================================================ --> + + <class name = "queue" handler = "channel" index = "50" label = "work with queues"> + <doc> + Queues store and forward messages. Queues can be configured in the server or created at + runtime. Queues must be attached to at least one exchange in order to receive messages + from publishers. + </doc> + + <doc type = "grammar"> + queue = C:DECLARE S:DECLARE-OK + / C:BIND S:BIND-OK + / C:UNBIND S:UNBIND-OK + / C:PURGE S:PURGE-OK + / C:DELETE S:DELETE-OK + </doc> + + <chassis name = "server" implement = "MUST" /> + <chassis name = "client" implement = "MUST" /> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "declare" synchronous = "1" index = "10" label = "declare queue, create if needed"> + <doc> + This method creates or checks a queue. When creating a new queue the client can + specify various properties that control the durability of the queue and its + contents, and the level of sharing for the queue. + </doc> + + <rule name = "default-binding"> + <doc> + The server MUST create a default binding for a newly-declared queue to the + default exchange, which is an exchange of type 'direct' and use the queue + name as the routing key. + </doc> + <doc type = "scenario"> + Client declares a new queue, and then without explicitly binding it to an + exchange, attempts to send a message through the default exchange binding, + i.e. publish a message to the empty exchange, with the queue name as routing + key. + </doc> + </rule> + + <rule name = "minimum-queues"> + <doc> + The server SHOULD support a minimum of 256 queues per virtual host and ideally, + impose no limit except as defined by available resources. + </doc> + <doc type = "scenario"> + Client attempts to declare as many queues as it can until the server reports + an error. The resulting count must at least be 256. + </doc> + </rule> + + <chassis name = "server" implement = "MUST" /> + <response name = "declare-ok" /> + + <!-- Deprecated: "ticket", must be zero --> + <field name = "reserved-1" type = "short" reserved = "1" /> + + <field name = "queue" domain = "queue-name"> + <rule name = "default-name"> + <doc> + The queue name MAY be empty, in which case the server MUST create a new + queue with a unique generated name and return this to the client in the + Declare-Ok method. + </doc> + <doc type = "scenario"> + Client attempts to declare several queues with an empty name. The client then + verifies that the server-assigned names are unique and different. + </doc> + </rule> + <rule name = "reserved" on-failure = "access-refused"> + <doc> + Queue names starting with "amq." are reserved for pre-declared and + standardised queues. The client MAY declare a queue starting with + "amq." if the passive option is set, or the queue already exists. + </doc> + <doc type = "scenario"> + The client attempts to declare a non-existing queue starting with + "amq." and with the passive option set to zero. + </doc> + </rule> + <rule name = "syntax" on-failure = "precondition-failed"> + <doc> + The queue name can be empty, or a sequence of these characters: + letters, digits, hyphen, underscore, period, or colon. + </doc> + <doc type = "scenario"> + The client attempts to declare a queue with an illegal name. + </doc> + </rule> + </field> + + <field name = "passive" domain = "bit" label = "do not create queue"> + <doc> + If set, the server will reply with Declare-Ok if the queue already + exists with the same name, and raise an error if not. The client can + use this to check whether a queue exists without modifying the + server state. When set, all other method fields except name and no-wait + are ignored. A declare with both passive and no-wait has no effect. + Arguments are compared for semantic equivalence. + </doc> + <rule name = "passive" on-failure = "not-found"> + <doc> + The client MAY ask the server to assert that a queue exists without + creating the queue if not. If the queue does not exist, the server + treats this as a failure. + </doc> + <doc type = "scenario"> + Client declares an existing queue with the passive option and expects + the server to respond with a declare-ok. Client then attempts to declare + a non-existent queue with the passive option, and the server must close + the channel with the correct reply-code. + </doc> + </rule> + <rule name = "equivalent"> + <doc> + If not set and the queue exists, the server MUST check that the + existing queue has the same values for durable, exclusive, auto-delete, + and arguments fields. The server MUST respond with Declare-Ok if the + requested queue matches these fields, and MUST raise a channel exception + if not. + </doc> + <doc type = "scenario"> + TODO. + </doc> + </rule> + </field> + + <field name = "durable" domain = "bit" label = "request a durable queue"> + <doc> + If set when creating a new queue, the queue will be marked as durable. Durable + queues remain active when a server restarts. Non-durable queues (transient + queues) are purged if/when a server restarts. Note that durable queues do not + necessarily hold persistent messages, although it does not make sense to send + persistent messages to a transient queue. + </doc> + + <rule name = "persistence"> + <doc>The server MUST recreate the durable queue after a restart.</doc> + + <doc type = "scenario"> + Client declares a durable queue. The server is then restarted. The client + then attempts to send a message to the queue. The message should be successfully + delivered. + </doc> + </rule> + + <rule name = "types"> + <doc>The server MUST support both durable and transient queues.</doc> + <doc type = "scenario"> + A client declares two named queues, one durable and one transient. + </doc> + </rule> + </field> + + <field name = "exclusive" domain = "bit" label = "request an exclusive queue"> + <doc> + Exclusive queues may only be accessed by the current connection, and are + deleted when that connection closes. Passive declaration of an exclusive + queue by other connections are not allowed. + </doc> + + <rule name = "types"> + <doc> + The server MUST support both exclusive (private) and non-exclusive (shared) + queues. + </doc> + <doc type = "scenario"> + A client declares two named queues, one exclusive and one non-exclusive. + </doc> + </rule> + + <rule name = "exclusive" on-failure = "resource-locked"> + <doc> + The client MAY NOT attempt to use a queue that was declared as exclusive + by another still-open connection. + </doc> + <doc type = "scenario"> + One client declares an exclusive queue. A second client on a different + connection attempts to declare, bind, consume, purge, delete, or declare + a queue of the same name. + </doc> + </rule> + </field> + + <field name = "auto-delete" domain = "bit" label = "auto-delete queue when unused"> + <doc> + If set, the queue is deleted when all consumers have finished using it. The last + consumer can be cancelled either explicitly or because its channel is closed. If + there was no consumer ever on the queue, it won't be deleted. Applications can + explicitly delete auto-delete queues using the Delete method as normal. + </doc> + + <rule name = "pre-existence"> + <doc> + The server MUST ignore the auto-delete field if the queue already exists. + </doc> + <doc type = "scenario"> + Client declares two named queues, one as auto-delete and one explicit-delete. + Client then attempts to declare the two queues using the same names again, + but reversing the value of the auto-delete field in each case. Verify that the + queues still exist with the original auto-delete flag values. + </doc> + </rule> + </field> + + <field name = "no-wait" domain = "no-wait" /> + + <field name = "arguments" domain = "table" label = "arguments for declaration"> + <doc> + A set of arguments for the declaration. The syntax and semantics of these + arguments depends on the server implementation. + </doc> + </field> + </method> + + <method name = "declare-ok" synchronous = "1" index = "11" label = "confirms a queue definition"> + <doc> + This method confirms a Declare method and confirms the name of the queue, essential + for automatically-named queues. + </doc> + + <chassis name = "client" implement = "MUST" /> + + <field name = "queue" domain = "queue-name"> + <doc> + Reports the name of the queue. If the server generated a queue name, this field + contains that name. + </doc> + <assert check = "notnull" /> + </field> + + <field name = "message-count" domain = "message-count" /> + + <field name = "consumer-count" domain = "long" label = "number of consumers"> + <doc> + Reports the number of active consumers for the queue. Note that consumers can + suspend activity (Channel.Flow) in which case they do not appear in this count. + </doc> + </field> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "bind" synchronous = "1" index = "20" label = "bind queue to an exchange"> + <doc> + This method binds a queue to an exchange. Until a queue is bound it will not + receive any messages. In a classic messaging model, store-and-forward queues + are bound to a direct exchange and subscription queues are bound to a topic + exchange. + </doc> + + <rule name = "duplicates"> + <doc> + A server MUST allow ignore duplicate bindings - that is, two or more bind + methods for a specific queue, with identical arguments - without treating these + as an error. + </doc> + <doc type = "scenario"> + A client binds a named queue to an exchange. The client then repeats the bind + (with identical arguments). + </doc> + </rule> + + <rule name = "unique"> + <doc> + A server MUST not deliver the same message more than once to a queue, even if + the queue has multiple bindings that match the message. + </doc> + <doc type = "scenario"> + A client declares a named queue and binds it using multiple bindings to the + amq.topic exchange. The client then publishes a message that matches all its + bindings. + </doc> + </rule> + + <rule name = "transient-exchange"> + <doc> + The server MUST allow a durable queue to bind to a transient exchange. + </doc> + <doc type = "scenario"> + A client declares a transient exchange. The client then declares a named durable + queue and then attempts to bind the transient exchange to the durable queue. + </doc> + </rule> + + <rule name = "durable-exchange"> + <doc> + Bindings of durable queues to durable exchanges are automatically durable + and the server MUST restore such bindings after a server restart. + </doc> + <doc type = "scenario"> + A server declares a named durable queue and binds it to a durable exchange. The + server is restarted. The client then attempts to use the queue/exchange combination. + </doc> + </rule> + + <rule name = "binding-count"> + <doc> + The server SHOULD support at least 4 bindings per queue, and ideally, impose no + limit except as defined by available resources. + </doc> + <doc type = "scenario"> + A client declares a named queue and attempts to bind it to 4 different + exchanges. + </doc> + </rule> + + <chassis name = "server" implement = "MUST" /> + + <response name = "bind-ok" /> + + <!-- Deprecated: "ticket", must be zero --> + <field name = "reserved-1" type = "short" reserved = "1" /> + + <field name = "queue" domain = "queue-name"> + <doc>Specifies the name of the queue to bind.</doc> + <rule name = "queue-known" on-failure = "not-found"> + <doc> + The client MUST either specify a queue name or have previously declared a + queue on the same channel + </doc> + <doc type = "scenario"> + The client opens a channel and attempts to bind an unnamed queue. + </doc> + </rule> + <rule name = "must-exist" on-failure = "not-found"> + <doc> + The client MUST NOT attempt to bind a queue that does not exist. + </doc> + <doc type = "scenario"> + The client attempts to bind a non-existent queue. + </doc> + </rule> + </field> + + <field name = "exchange" domain = "exchange-name" label = "name of the exchange to bind to"> + <rule name = "exchange-existence" on-failure = "not-found"> + <doc> + A client MUST NOT be allowed to bind a queue to a non-existent exchange. + </doc> + <doc type = "scenario"> + A client attempts to bind an named queue to a undeclared exchange. + </doc> + </rule> + <rule name = "default-exchange"> + <doc> + The server MUST accept a blank exchange name to mean the default exchange. + </doc> + <doc type = "scenario"> + The client declares a queue and binds it to a blank exchange name. + </doc> + </rule> + </field> + + <field name = "routing-key" domain = "shortstr" label = "message routing key"> + <doc> + Specifies the routing key for the binding. The routing key is used for routing + messages depending on the exchange configuration. Not all exchanges use a + routing key - refer to the specific exchange documentation. If the queue name + is empty, the server uses the last queue declared on the channel. If the + routing key is also empty, the server uses this queue name for the routing + key as well. If the queue name is provided but the routing key is empty, the + server does the binding with that empty routing key. The meaning of empty + routing keys depends on the exchange implementation. + </doc> + <rule name = "direct-exchange-key-matching"> + <doc> + If a message queue binds to a direct exchange using routing key K and a + publisher sends the exchange a message with routing key R, then the message + MUST be passed to the message queue if K = R. + </doc> + </rule> + </field> + + <field name = "no-wait" domain = "no-wait" /> + + <field name = "arguments" domain = "table" label = "arguments for binding"> + <doc> + A set of arguments for the binding. The syntax and semantics of these arguments + depends on the exchange class. + </doc> + </field> + </method> + + <method name = "bind-ok" synchronous = "1" index = "21" label = "confirm bind successful"> + <doc>This method confirms that the bind was successful.</doc> + + <chassis name = "client" implement = "MUST" /> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "unbind" synchronous = "1" index = "50" label = "unbind a queue from an exchange"> + <doc>This method unbinds a queue from an exchange.</doc> + <rule name = "01"> + <doc>If a unbind fails, the server MUST raise a connection exception.</doc> + </rule> + <chassis name="server" implement="MUST"/> + <response name="unbind-ok"/> + + <!-- Deprecated: "ticket", must be zero --> + <field name = "reserved-1" type = "short" reserved = "1" /> + + <field name = "queue" domain = "queue-name"> + <doc>Specifies the name of the queue to unbind.</doc> + <rule name = "queue-known" on-failure = "not-found"> + <doc> + The client MUST either specify a queue name or have previously declared a + queue on the same channel + </doc> + <doc type = "scenario"> + The client opens a channel and attempts to unbind an unnamed queue. + </doc> + </rule> + <rule name = "must-exist" on-failure = "not-found"> + <doc> + The client MUST NOT attempt to unbind a queue that does not exist. + </doc> + <doc type = "scenario"> + The client attempts to unbind a non-existent queue. + </doc> + </rule> + </field> + + <field name = "exchange" domain = "exchange-name"> + <doc>The name of the exchange to unbind from.</doc> + <rule name = "must-exist" on-failure = "not-found"> + <doc> + The client MUST NOT attempt to unbind a queue from an exchange that + does not exist. + </doc> + <doc type = "scenario"> + The client attempts to unbind a queue from a non-existent exchange. + </doc> + </rule> + <rule name = "default-exchange"> + <doc> + The server MUST accept a blank exchange name to mean the default exchange. + </doc> + <doc type = "scenario"> + The client declares a queue and binds it to a blank exchange name. + </doc> + </rule> + </field> + + <field name = "routing-key" domain = "shortstr" label = "routing key of binding"> + <doc>Specifies the routing key of the binding to unbind.</doc> + </field> + + <field name = "arguments" domain = "table" label = "arguments of binding"> + <doc>Specifies the arguments of the binding to unbind.</doc> + </field> + </method> + + <method name = "unbind-ok" synchronous = "1" index = "51" label = "confirm unbind successful"> + <doc>This method confirms that the unbind was successful.</doc> + <chassis name = "client" implement = "MUST"/> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "purge" synchronous = "1" index = "30" label = "purge a queue"> + <doc> + This method removes all messages from a queue which are not awaiting + acknowledgment. + </doc> + + <rule name = "02"> + <doc> + The server MUST NOT purge messages that have already been sent to a client + but not yet acknowledged. + </doc> + </rule> + + <rule name = "03"> + <doc> + The server MAY implement a purge queue or log that allows system administrators + to recover accidentally-purged messages. The server SHOULD NOT keep purged + messages in the same storage spaces as the live messages since the volumes of + purged messages may get very large. + </doc> + </rule> + + <chassis name = "server" implement = "MUST" /> + + <response name = "purge-ok" /> + + <!-- Deprecated: "ticket", must be zero --> + <field name = "reserved-1" type = "short" reserved = "1" /> + + <field name = "queue" domain = "queue-name"> + <doc>Specifies the name of the queue to purge.</doc> + <rule name = "queue-known" on-failure = "not-found"> + <doc> + The client MUST either specify a queue name or have previously declared a + queue on the same channel + </doc> + <doc type = "scenario"> + The client opens a channel and attempts to purge an unnamed queue. + </doc> + </rule> + <rule name = "must-exist" on-failure = "not-found"> + <doc> + The client MUST NOT attempt to purge a queue that does not exist. + </doc> + <doc type = "scenario"> + The client attempts to purge a non-existent queue. + </doc> + </rule> + </field> + + <field name = "no-wait" domain = "no-wait" /> + </method> + + <method name = "purge-ok" synchronous = "1" index = "31" label = "confirms a queue purge"> + <doc>This method confirms the purge of a queue.</doc> + + <chassis name = "client" implement = "MUST" /> + + <field name = "message-count" domain = "message-count"> + <doc> + Reports the number of messages purged. + </doc> + </field> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "delete" synchronous = "1" index = "40" label = "delete a queue"> + <doc> + This method deletes a queue. When a queue is deleted any pending messages are sent + to a dead-letter queue if this is defined in the server configuration, and all + consumers on the queue are cancelled. + </doc> + + <rule name = "01"> + <doc> + The server SHOULD use a dead-letter queue to hold messages that were pending on + a deleted queue, and MAY provide facilities for a system administrator to move + these messages back to an active queue. + </doc> + </rule> + + <chassis name = "server" implement = "MUST" /> + + <response name = "delete-ok" /> + + <!-- Deprecated: "ticket", must be zero --> + <field name = "reserved-1" type = "short" reserved = "1" /> + + <field name = "queue" domain = "queue-name"> + <doc>Specifies the name of the queue to delete.</doc> + <rule name = "queue-known" on-failure = "not-found"> + <doc> + The client MUST either specify a queue name or have previously declared a + queue on the same channel + </doc> + <doc type = "scenario"> + The client opens a channel and attempts to delete an unnamed queue. + </doc> + </rule> + <rule name = "must-exist" on-failure = "not-found"> + <doc> + The client MUST NOT attempt to delete a queue that does not exist. + </doc> + <doc type = "scenario"> + The client attempts to delete a non-existent queue. + </doc> + </rule> + </field> + + <field name = "if-unused" domain = "bit" label = "delete only if unused"> + <doc> + If set, the server will only delete the queue if it has no consumers. If the + queue has consumers the server does does not delete it but raises a channel + exception instead. + </doc> + <rule name = "in-use" on-failure = "precondition-failed"> + <doc> + The server MUST NOT delete a queue that has consumers on it, if the if-unused + field is true. + </doc> + <doc type = "scenario"> + The client declares a queue, and consumes from it, then tries to delete it + setting if-unused to true. + </doc> + </rule> + </field> + + <field name = "if-empty" domain = "bit" label = "delete only if empty"> + <doc> + If set, the server will only delete the queue if it has no messages. + </doc> + <rule name = "not-empty" on-failure = "precondition-failed"> + <doc> + The server MUST NOT delete a queue that has messages on it, if the + if-empty field is true. + </doc> + <doc type = "scenario"> + The client declares a queue, binds it and publishes some messages into it, + then tries to delete it setting if-empty to true. + </doc> + </rule> + </field> + + <field name = "no-wait" domain = "no-wait" /> + </method> + + <method name = "delete-ok" synchronous = "1" index = "41" label = "confirm deletion of a queue"> + <doc>This method confirms the deletion of a queue.</doc> + + <chassis name = "client" implement = "MUST" /> + + <field name = "message-count" domain = "message-count"> + <doc>Reports the number of messages deleted.</doc> + </field> + </method> + </class> + + <!-- == BASIC ============================================================ --> + + <class name = "basic" handler = "channel" index = "60" label = "work with basic content"> + <doc> + The Basic class provides methods that support an industry-standard messaging model. + </doc> + + <doc type = "grammar"> + basic = C:QOS S:QOS-OK + / C:CONSUME S:CONSUME-OK + / C:CANCEL S:CANCEL-OK + / C:PUBLISH content + / S:RETURN content + / S:DELIVER content + / C:GET ( S:GET-OK content / S:GET-EMPTY ) + / C:ACK + / C:REJECT + / C:RECOVER-ASYNC + / C:RECOVER S:RECOVER-OK + </doc> + + <chassis name = "server" implement = "MUST" /> + <chassis name = "client" implement = "MAY" /> + + <rule name = "01"> + <doc> + The server SHOULD respect the persistent property of basic messages and + SHOULD make a best-effort to hold persistent basic messages on a reliable + storage mechanism. + </doc> + <doc type = "scenario"> + Send a persistent message to queue, stop server, restart server and then + verify whether message is still present. Assumes that queues are durable. + Persistence without durable queues makes no sense. + </doc> + </rule> + + <rule name = "02"> + <doc> + The server MUST NOT discard a persistent basic message in case of a queue + overflow. + </doc> + <doc type = "scenario"> + Declare a queue overflow situation with persistent messages and verify that + messages do not get lost (presumably the server will write them to disk). + </doc> + </rule> + + <rule name = "03"> + <doc> + The server MAY use the Channel.Flow method to slow or stop a basic message + publisher when necessary. + </doc> + <doc type = "scenario"> + Declare a queue overflow situation with non-persistent messages and verify + whether the server responds with Channel.Flow or not. Repeat with persistent + messages. + </doc> + </rule> + + <rule name = "04"> + <doc> + The server MAY overflow non-persistent basic messages to persistent + storage. + </doc> + <!-- Test scenario: untestable --> + </rule> + + <rule name = "05"> + <doc> + The server MAY discard or dead-letter non-persistent basic messages on a + priority basis if the queue size exceeds some configured limit. + </doc> + <!-- Test scenario: untestable --> + </rule> + + <rule name = "06"> + <doc> + The server MUST implement at least 2 priority levels for basic messages, + where priorities 0-4 and 5-9 are treated as two distinct levels. + </doc> + <doc type = "scenario"> + Send a number of priority 0 messages to a queue. Send one priority 9 + message. Consume messages from the queue and verify that the first message + received was priority 9. + </doc> + </rule> + + <rule name = "07"> + <doc> + The server MAY implement up to 10 priority levels. + </doc> + <doc type = "scenario"> + Send a number of messages with mixed priorities to a queue, so that all + priority values from 0 to 9 are exercised. A good scenario would be ten + messages in low-to-high priority. Consume from queue and verify how many + priority levels emerge. + </doc> + </rule> + + <rule name = "08"> + <doc> + The server MUST deliver messages of the same priority in order irrespective of + their individual persistence. + </doc> + <doc type = "scenario"> + Send a set of messages with the same priority but different persistence + settings to a queue. Consume and verify that messages arrive in same order + as originally published. + </doc> + </rule> + + <rule name = "09"> + <doc> + The server MUST support un-acknowledged delivery of Basic content, i.e. + consumers with the no-ack field set to TRUE. + </doc> + </rule> + + <rule name = "10"> + <doc> + The server MUST support explicitly acknowledged delivery of Basic content, + i.e. consumers with the no-ack field set to FALSE. + </doc> + <doc type = "scenario"> + Declare a queue and a consumer using explicit acknowledgements. Publish a + set of messages to the queue. Consume the messages but acknowledge only + half of them. Disconnect and reconnect, and consume from the queue. + Verify that the remaining messages are received. + </doc> + </rule> + + <!-- These are the properties for a Basic content --> + + <!-- MIME typing --> + <field name = "content-type" domain = "shortstr" label = "MIME content type" /> + <!-- MIME typing --> + <field name = "content-encoding" domain = "shortstr" label = "MIME content encoding" /> + <!-- For applications, and for header exchange routing --> + <field name = "headers" domain = "table" label = "message header field table" /> + <!-- For queues that implement persistence --> + <field name = "delivery-mode" domain = "octet" label = "non-persistent (1) or persistent (2)" /> + <!-- For queues that implement priorities --> + <field name = "priority" domain = "octet" label = "message priority, 0 to 9" /> + <!-- For application use, no formal behaviour --> + <field name = "correlation-id" domain = "shortstr" label = "application correlation identifier" /> + <!-- For application use, no formal behaviour but may hold the + name of a private response queue, when used in request messages --> + <field name = "reply-to" domain = "shortstr" label = "address to reply to" /> + <!-- For implementation use, no formal behaviour --> + <field name = "expiration" domain = "shortstr" label = "message expiration specification" /> + <!-- For application use, no formal behaviour --> + <field name = "message-id" domain = "shortstr" label = "application message identifier" /> + <!-- For application use, no formal behaviour --> + <field name = "timestamp" domain = "timestamp" label = "message timestamp" /> + <!-- For application use, no formal behaviour --> + <field name = "type" domain = "shortstr" label = "message type name" /> + <!-- For application use, no formal behaviour --> + <field name = "user-id" domain = "shortstr" label = "creating user id" /> + <!-- For application use, no formal behaviour --> + <field name = "app-id" domain = "shortstr" label = "creating application id" /> + <!-- Deprecated, was old cluster-id property --> + <field name = "reserved" domain = "shortstr" label = "reserved, must be empty" /> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "qos" synchronous = "1" index = "10" label = "specify quality of service"> + <doc> + This method requests a specific quality of service. The QoS can be specified for the + current channel or for all channels on the connection. The particular properties and + semantics of a qos method always depend on the content class semantics. Though the + qos method could in principle apply to both peers, it is currently meaningful only + for the server. + </doc> + + <chassis name = "server" implement = "MUST" /> + <response name = "qos-ok" /> + + <field name = "prefetch-size" domain = "long" label = "prefetch window in octets"> + <doc> + The client can request that messages be sent in advance so that when the client + finishes processing a message, the following message is already held locally, + rather than needing to be sent down the channel. Prefetching gives a performance + improvement. This field specifies the prefetch window size in octets. The server + will send a message in advance if it is equal to or smaller in size than the + available prefetch size (and also falls into other prefetch limits). May be set + to zero, meaning "no specific limit", although other prefetch limits may still + apply. The prefetch-size is ignored if the no-ack option is set. + </doc> + <rule name = "01"> + <doc> + The server MUST ignore this setting when the client is not processing any + messages - i.e. the prefetch size does not limit the transfer of single + messages to a client, only the sending in advance of more messages while + the client still has one or more unacknowledged messages. + </doc> + <doc type = "scenario"> + Define a QoS prefetch-size limit and send a single message that exceeds + that limit. Verify that the message arrives correctly. + </doc> + </rule> + </field> + + <field name = "prefetch-count" domain = "short" label = "prefetch window in messages"> + <doc> + Specifies a prefetch window in terms of whole messages. This field may be used + in combination with the prefetch-size field; a message will only be sent in + advance if both prefetch windows (and those at the channel and connection level) + allow it. The prefetch-count is ignored if the no-ack option is set. + </doc> + <rule name = "01"> + <doc> + The server may send less data in advance than allowed by the client's + specified prefetch windows but it MUST NOT send more. + </doc> + <doc type = "scenario"> + Define a QoS prefetch-size limit and a prefetch-count limit greater than + one. Send multiple messages that exceed the prefetch size. Verify that + no more than one message arrives at once. + </doc> + </rule> + </field> + + <field name = "global" domain = "bit" label = "apply to entire connection"> + <doc> + By default the QoS settings apply to the current channel only. If this field is + set, they are applied to the entire connection. + </doc> + </field> + </method> + + <method name = "qos-ok" synchronous = "1" index = "11" label = "confirm the requested qos"> + <doc> + This method tells the client that the requested QoS levels could be handled by the + server. The requested QoS applies to all active consumers until a new QoS is + defined. + </doc> + <chassis name = "client" implement = "MUST" /> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "consume" synchronous = "1" index = "20" label = "start a queue consumer"> + <doc> + This method asks the server to start a "consumer", which is a transient request for + messages from a specific queue. Consumers last as long as the channel they were + declared on, or until the client cancels them. + </doc> + + <rule name = "01"> + <doc> + The server SHOULD support at least 16 consumers per queue, and ideally, impose + no limit except as defined by available resources. + </doc> + <doc type = "scenario"> + Declare a queue and create consumers on that queue until the server closes the + connection. Verify that the number of consumers created was at least sixteen + and report the total number. + </doc> + </rule> + + <chassis name = "server" implement = "MUST" /> + <response name = "consume-ok" /> + + <!-- Deprecated: "ticket", must be zero --> + <field name = "reserved-1" type = "short" reserved = "1" /> + + <field name = "queue" domain = "queue-name"> + <doc>Specifies the name of the queue to consume from.</doc> + </field> + + <field name = "consumer-tag" domain = "consumer-tag"> + <doc> + Specifies the identifier for the consumer. The consumer tag is local to a + channel, so two clients can use the same consumer tags. If this field is + empty the server will generate a unique tag. + </doc> + <rule name = "01" on-failure = "not-allowed"> + <doc> + The client MUST NOT specify a tag that refers to an existing consumer. + </doc> + <doc type = "scenario"> + Attempt to create two consumers with the same non-empty tag, on the + same channel. + </doc> + </rule> + <rule name = "02" on-failure = "not-allowed"> + <doc> + The consumer tag is valid only within the channel from which the + consumer was created. I.e. a client MUST NOT create a consumer in one + channel and then use it in another. + </doc> + <doc type = "scenario"> + Attempt to create a consumer in one channel, then use in another channel, + in which consumers have also been created (to test that the server uses + unique consumer tags). + </doc> + </rule> + </field> + + <field name = "no-local" domain = "no-local" /> + + <field name = "no-ack" domain = "no-ack" /> + + <field name = "exclusive" domain = "bit" label = "request exclusive access"> + <doc> + Request exclusive consumer access, meaning only this consumer can access the + queue. + </doc> + + <rule name = "01" on-failure = "access-refused"> + <doc> + The client MAY NOT gain exclusive access to a queue that already has + active consumers. + </doc> + <doc type = "scenario"> + Open two connections to a server, and in one connection declare a shared + (non-exclusive) queue and then consume from the queue. In the second + connection attempt to consume from the same queue using the exclusive + option. + </doc> + </rule> + </field> + + <field name = "no-wait" domain = "no-wait" /> + + <field name = "arguments" domain = "table" label = "arguments for declaration"> + <doc> + A set of arguments for the consume. The syntax and semantics of these + arguments depends on the server implementation. + </doc> + </field> + </method> + + <method name = "consume-ok" synchronous = "1" index = "21" label = "confirm a new consumer"> + <doc> + The server provides the client with a consumer tag, which is used by the client + for methods called on the consumer at a later stage. + </doc> + <chassis name = "client" implement = "MUST" /> + <field name = "consumer-tag" domain = "consumer-tag"> + <doc> + Holds the consumer tag specified by the client or provided by the server. + </doc> + </field> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "cancel" synchronous = "1" index = "30" label = "end a queue consumer"> + <doc> + This method cancels a consumer. This does not affect already delivered + messages, but it does mean the server will not send any more messages for + that consumer. The client may receive an arbitrary number of messages in + between sending the cancel method and receiving the cancel-ok reply. + </doc> + + <rule name = "01"> + <doc> + If the queue does not exist the server MUST ignore the cancel method, so + long as the consumer tag is valid for that channel. + </doc> + <doc type = "scenario"> + TODO. + </doc> + </rule> + + <chassis name = "server" implement = "MUST" /> + <response name = "cancel-ok" /> + + <field name = "consumer-tag" domain = "consumer-tag" /> + <field name = "no-wait" domain = "no-wait" /> + </method> + + <method name = "cancel-ok" synchronous = "1" index = "31" label = "confirm a cancelled consumer"> + <doc> + This method confirms that the cancellation was completed. + </doc> + <chassis name = "client" implement = "MUST" /> + <field name = "consumer-tag" domain = "consumer-tag" /> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "publish" content = "1" index = "40" label = "publish a message"> + <doc> + This method publishes a message to a specific exchange. The message will be routed + to queues as defined by the exchange configuration and distributed to any active + consumers when the transaction, if any, is committed. + </doc> + + <chassis name = "server" implement = "MUST" /> + + <!-- Deprecated: "ticket", must be zero --> + <field name = "reserved-1" type = "short" reserved = "1" /> + + <field name = "exchange" domain = "exchange-name"> + <doc> + Specifies the name of the exchange to publish to. The exchange name can be + empty, meaning the default exchange. If the exchange name is specified, and that + exchange does not exist, the server will raise a channel exception. + </doc> + + <rule name = "must-exist" on-failure = "not-found"> + <doc> + The client MUST NOT attempt to publish a content to an exchange that + does not exist. + </doc> + <doc type = "scenario"> + The client attempts to publish a content to a non-existent exchange. + </doc> + </rule> + <rule name = "default-exchange"> + <doc> + The server MUST accept a blank exchange name to mean the default exchange. + </doc> + <doc type = "scenario"> + The client declares a queue and binds it to a blank exchange name. + </doc> + </rule> + <rule name = "02"> + <doc> + If the exchange was declared as an internal exchange, the server MUST raise + a channel exception with a reply code 403 (access refused). + </doc> + <doc type = "scenario"> + TODO. + </doc> + </rule> + + <rule name = "03"> + <doc> + The exchange MAY refuse basic content in which case it MUST raise a channel + exception with reply code 540 (not implemented). + </doc> + <doc type = "scenario"> + TODO. + </doc> + </rule> + </field> + + <field name = "routing-key" domain = "shortstr" label = "Message routing key"> + <doc> + Specifies the routing key for the message. The routing key is used for routing + messages depending on the exchange configuration. + </doc> + </field> + + <field name = "mandatory" domain = "bit" label = "indicate mandatory routing"> + <doc> + This flag tells the server how to react if the message cannot be routed to a + queue. If this flag is set, the server will return an unroutable message with a + Return method. If this flag is zero, the server silently drops the message. + </doc> + + <rule name = "01"> + <doc> + The server SHOULD implement the mandatory flag. + </doc> + <doc type = "scenario"> + TODO. + </doc> + </rule> + </field> + + <field name = "immediate" domain = "bit" label = "request immediate delivery"> + <doc> + This flag tells the server how to react if the message cannot be routed to a + queue consumer immediately. If this flag is set, the server will return an + undeliverable message with a Return method. If this flag is zero, the server + will queue the message, but with no guarantee that it will ever be consumed. + </doc> + + <rule name = "01"> + <doc> + The server SHOULD implement the immediate flag. + </doc> + <doc type = "scenario"> + TODO. + </doc> + </rule> + </field> + </method> + + <method name = "return" content = "1" index = "50" label = "return a failed message"> + <doc> + This method returns an undeliverable message that was published with the "immediate" + flag set, or an unroutable message published with the "mandatory" flag set. The + reply code and text provide information about the reason that the message was + undeliverable. + </doc> + + <chassis name = "client" implement = "MUST" /> + + <field name = "reply-code" domain = "reply-code" /> + <field name = "reply-text" domain = "reply-text" /> + + <field name = "exchange" domain = "exchange-name"> + <doc> + Specifies the name of the exchange that the message was originally published + to. May be empty, meaning the default exchange. + </doc> + </field> + + <field name = "routing-key" domain = "shortstr" label = "Message routing key"> + <doc> + Specifies the routing key name specified when the message was published. + </doc> + </field> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "deliver" content = "1" index = "60" + label = "notify the client of a consumer message"> + <doc> + This method delivers a message to the client, via a consumer. In the asynchronous + message delivery model, the client starts a consumer using the Consume method, then + the server responds with Deliver methods as and when messages arrive for that + consumer. + </doc> + + <rule name = "01"> + <doc> + The server SHOULD track the number of times a message has been delivered to + clients and when a message is redelivered a certain number of times - e.g. 5 + times - without being acknowledged, the server SHOULD consider the message to be + unprocessable (possibly causing client applications to abort), and move the + message to a dead letter queue. + </doc> + <doc type = "scenario"> + TODO. + </doc> + </rule> + + <chassis name = "client" implement = "MUST" /> + + <field name = "consumer-tag" domain = "consumer-tag" /> + <field name = "delivery-tag" domain = "delivery-tag" /> + <field name = "redelivered" domain = "redelivered" /> + + <field name = "exchange" domain = "exchange-name"> + <doc> + Specifies the name of the exchange that the message was originally published to. + May be empty, indicating the default exchange. + </doc> + </field> + + <field name = "routing-key" domain = "shortstr" label = "Message routing key"> + <doc>Specifies the routing key name specified when the message was published.</doc> + </field> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "get" synchronous = "1" index = "70" label = "direct access to a queue"> + <doc> + This method provides a direct access to the messages in a queue using a synchronous + dialogue that is designed for specific types of application where synchronous + functionality is more important than performance. + </doc> + + <response name = "get-ok" /> + <response name = "get-empty" /> + <chassis name = "server" implement = "MUST" /> + + <!-- Deprecated: "ticket", must be zero --> + <field name = "reserved-1" type = "short" reserved = "1" /> + + <field name = "queue" domain = "queue-name"> + <doc>Specifies the name of the queue to get a message from.</doc> + </field> + <field name = "no-ack" domain = "no-ack" /> + </method> + + <method name = "get-ok" synchronous = "1" content = "1" index = "71" + label = "provide client with a message"> + <doc> + This method delivers a message to the client following a get method. A message + delivered by 'get-ok' must be acknowledged unless the no-ack option was set in the + get method. + </doc> + + <chassis name = "client" implement = "MAY" /> + + <field name = "delivery-tag" domain = "delivery-tag" /> + <field name = "redelivered" domain = "redelivered" /> + <field name = "exchange" domain = "exchange-name"> + <doc> + Specifies the name of the exchange that the message was originally published to. + If empty, the message was published to the default exchange. + </doc> + </field> + + <field name = "routing-key" domain = "shortstr" label = "Message routing key"> + <doc>Specifies the routing key name specified when the message was published.</doc> + </field> + + <field name = "message-count" domain = "message-count" /> + </method> + + <method name = "get-empty" synchronous = "1" index = "72" + label = "indicate no messages available"> + <doc> + This method tells the client that the queue has no messages available for the + client. + </doc> + <chassis name = "client" implement = "MAY" /> + <!-- Deprecated: "cluster-id", must be empty --> + <field name = "reserved-1" type = "shortstr" reserved = "1" /> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "ack" index = "80" label = "acknowledge one or more messages"> + <doc> + This method acknowledges one or more messages delivered via the Deliver or Get-Ok + methods. The client can ask to confirm a single message or a set of messages up to + and including a specific message. + </doc> + + <chassis name = "server" implement = "MUST" /> + + <field name = "delivery-tag" domain = "delivery-tag" /> + <field name = "multiple" domain = "bit" label = "acknowledge multiple messages"> + <doc> + If set to 1, the delivery tag is treated as "up to and including", so that the + client can acknowledge multiple messages with a single method. If set to zero, + the delivery tag refers to a single message. If the multiple field is 1, and the + delivery tag is zero, tells the server to acknowledge all outstanding messages. + </doc> + <rule name = "exists" on-failure = "precondition-failed"> + <doc> + The server MUST validate that a non-zero delivery-tag refers to a delivered + message, and raise a channel exception if this is not the case. On a transacted + channel, this check MUST be done immediately and not delayed until a Tx.Commit. + Specifically, a client MUST not acknowledge the same message more than once. + </doc> + <doc type = "scenario"> + TODO. + </doc> + </rule> + </field> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "reject" index = "90" label = "reject an incoming message"> + <doc> + This method allows a client to reject a message. It can be used to interrupt and + cancel large incoming messages, or return untreatable messages to their original + queue. + </doc> + + <rule name = "01"> + <doc> + The server SHOULD be capable of accepting and process the Reject method while + sending message content with a Deliver or Get-Ok method. I.e. the server should + read and process incoming methods while sending output frames. To cancel a + partially-send content, the server sends a content body frame of size 1 (i.e. + with no data except the frame-end octet). + </doc> + </rule> + + <rule name = "02"> + <doc> + The server SHOULD interpret this method as meaning that the client is unable to + process the message at this time. + </doc> + <doc type = "scenario"> + TODO. + </doc> + </rule> + + <rule name = "03"> + <doc> + The client MUST NOT use this method as a means of selecting messages to process. + </doc> + <doc type = "scenario"> + TODO. + </doc> + </rule> + + <chassis name = "server" implement = "MUST" /> + + <field name = "delivery-tag" domain = "delivery-tag" /> + + <field name = "requeue" domain = "bit" label = "requeue the message"> + <doc> + If requeue is true, the server will attempt to requeue the message. If requeue + is false or the requeue attempt fails the messages are discarded or dead-lettered. + </doc> + + <rule name = "01"> + <doc> + The server MUST NOT deliver the message to the same client within the + context of the current channel. The recommended strategy is to attempt to + deliver the message to an alternative consumer, and if that is not possible, + to move the message to a dead-letter queue. The server MAY use more + sophisticated tracking to hold the message on the queue and redeliver it to + the same client at a later stage. + </doc> + <doc type = "scenario"> + TODO. + </doc> + </rule> + </field> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "recover-async" index = "100" label = "redeliver unacknowledged messages" + deprecated = "1"> + <doc> + This method asks the server to redeliver all unacknowledged messages on a + specified channel. Zero or more messages may be redelivered. This method + is deprecated in favour of the synchronous Recover/Recover-Ok. + </doc> + <rule name = "01"> + <doc> + The server MUST set the redelivered flag on all messages that are resent. + </doc> + <doc type = "scenario"> + TODO. + </doc> + </rule> + <chassis name = "server" implement = "MAY" /> + <field name = "requeue" domain = "bit" label = "requeue the message"> + <doc> + If this field is zero, the message will be redelivered to the original + recipient. If this bit is 1, the server will attempt to requeue the message, + potentially then delivering it to an alternative subscriber. + </doc> + </field> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "recover" index = "110" label = "redeliver unacknowledged messages"> + <doc> + This method asks the server to redeliver all unacknowledged messages on a + specified channel. Zero or more messages may be redelivered. This method + replaces the asynchronous Recover. + </doc> + <rule name = "01"> + <doc> + The server MUST set the redelivered flag on all messages that are resent. + </doc> + <doc type = "scenario"> + TODO. + </doc> + </rule> + <chassis name = "server" implement = "MUST" /> + <field name = "requeue" domain = "bit" label = "requeue the message"> + <doc> + If this field is zero, the message will be redelivered to the original + recipient. If this bit is 1, the server will attempt to requeue the message, + potentially then delivering it to an alternative subscriber. + </doc> + </field> + </method> + + <method name = "recover-ok" synchronous = "1" index = "111" label = "confirm recovery"> + <doc> + This method acknowledges a Basic.Recover method. + </doc> + <chassis name = "client" implement = "MUST" /> + </method> + </class> + + <!-- == TX =============================================================== --> + + <class name = "tx" handler = "channel" index = "90" label = "work with transactions"> + <doc> + The Tx class allows publish and ack operations to be batched into atomic + units of work. The intention is that all publish and ack requests issued + within a transaction will complete successfully or none of them will. + Servers SHOULD implement atomic transactions at least where all publish + or ack requests affect a single queue. Transactions that cover multiple + queues may be non-atomic, given that queues can be created and destroyed + asynchronously, and such events do not form part of any transaction. + Further, the behaviour of transactions with respect to the immediate and + mandatory flags on Basic.Publish methods is not defined. + </doc> + + <rule name = "not multiple queues"> + <doc> + Applications MUST NOT rely on the atomicity of transactions that + affect more than one queue. + </doc> + </rule> + <rule name = "not immediate"> + <doc> + Applications MUST NOT rely on the behaviour of transactions that + include messages published with the immediate option. + </doc> + </rule> + <rule name = "not mandatory"> + <doc> + Applications MUST NOT rely on the behaviour of transactions that + include messages published with the mandatory option. + </doc> + </rule> + + <doc type = "grammar"> + tx = C:SELECT S:SELECT-OK + / C:COMMIT S:COMMIT-OK + / C:ROLLBACK S:ROLLBACK-OK + </doc> + + <chassis name = "server" implement = "SHOULD" /> + <chassis name = "client" implement = "MAY" /> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "select" synchronous = "1" index = "10" label = "select standard transaction mode"> + <doc> + This method sets the channel to use standard transactions. The client must use this + method at least once on a channel before using the Commit or Rollback methods. + </doc> + <chassis name = "server" implement = "MUST" /> + <response name = "select-ok" /> + </method> + + <method name = "select-ok" synchronous = "1" index = "11" label = "confirm transaction mode"> + <doc> + This method confirms to the client that the channel was successfully set to use + standard transactions. + </doc> + <chassis name = "client" implement = "MUST" /> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "commit" synchronous = "1" index = "20" label = "commit the current transaction"> + <doc> + This method commits all message publications and acknowledgments performed in + the current transaction. A new transaction starts immediately after a commit. + </doc> + <chassis name = "server" implement = "MUST" /> + <response name = "commit-ok" /> + + <rule name = "transacted" on-failure = "precondition-failed"> + <doc> + The client MUST NOT use the Commit method on non-transacted channels. + </doc> + <doc type = "scenario"> + The client opens a channel and then uses Tx.Commit. + </doc> + </rule> + </method> + + <method name = "commit-ok" synchronous = "1" index = "21" label = "confirm a successful commit"> + <doc> + This method confirms to the client that the commit succeeded. Note that if a commit + fails, the server raises a channel exception. + </doc> + <chassis name = "client" implement = "MUST" /> + </method> + + <!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - --> + + <method name = "rollback" synchronous = "1" index = "30" + label = "abandon the current transaction"> + <doc> + This method abandons all message publications and acknowledgments performed in + the current transaction. A new transaction starts immediately after a rollback. + Note that unacked messages will not be automatically redelivered by rollback; + if that is required an explicit recover call should be issued. + </doc> + <chassis name = "server" implement = "MUST" /> + <response name = "rollback-ok" /> + + <rule name = "transacted" on-failure = "precondition-failed"> + <doc> + The client MUST NOT use the Rollback method on non-transacted channels. + </doc> + <doc type = "scenario"> + The client opens a channel and then uses Tx.Rollback. + </doc> + </rule> + </method> + + <method name = "rollback-ok" synchronous = "1" index = "31" label = "confirm successful rollback"> + <doc> + This method confirms to the client that the rollback succeeded. Note that if an + rollback fails, the server raises a channel exception. + </doc> + <chassis name = "client" implement = "MUST" /> + </method> + </class> + +</amqp> diff --git a/setup.cfg b/setup.cfg index 81af2bb8d76f960564e3394a03f6f2b6c290ffe9..cdd41f09fc5bca6d9a07e432abcb45b64a7ea694 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,3 +3,6 @@ description-file = README.md [pycodestyle] max-line-length=120 + +[bdist_wheel] +universal=1 diff --git a/setup.py b/setup.py index fe3b9ae87d3a24da0152382bc308d61695e80e82..c438ee99013b6c7f331015190563f5e4756e816c 100644 --- a/setup.py +++ b/setup.py @@ -2,8 +2,9 @@ # coding=UTF-8 from setuptools import setup + setup(name='CoolAMQP', - version='0.12', + version='0.8', description='AMQP client with sane reconnects', author='DMS Serwis s.c.', author_email='piotrm@smok.co', @@ -12,7 +13,12 @@ setup(name='CoolAMQP', keywords=['amqp', 'pyamqp', 'rabbitmq', 'client', 'network', 'ha', 'high availability'], packages=[ 'coolamqp', - 'coolamqp.backends' + 'coolamqp.backends', + 'coolamqp.uplink', + 'coolamqp.uplink.framing', + 'coolamqp.uplink.framing.compilation', + 'coolamqp.uplink.streams', + ], license='MIT License', long_description=u'The AMQP client that handles reconnection madness for you', diff --git a/tests/run.py b/tests/run.py new file mode 100644 index 0000000000000000000000000000000000000000..f7fe4c6077fe797bf56791b0a4d71251980a309b --- /dev/null +++ b/tests/run.py @@ -0,0 +1,24 @@ +# coding=UTF-8 +from __future__ import absolute_import, division, print_function +import time, logging, threading +from coolamqp.objects import Message, MessageProperties, NodeDefinition, Queue +from coolamqp.clustering import Cluster + +import time + + +NODE = NodeDefinition('127.0.0.1', 'user', 'user', heartbeat=20) +logging.basicConfig(level=logging.DEBUG) + +if __name__ == '__main__': + amqp = Cluster([NODE]) + amqp.start(wait=True) + + + c1 = amqp.consume(Queue(b'siema-eniu', exclusive=True), qos=(None, 20)) + c2 = amqp.consume(Queue(b'jo-malina', exclusive=True)) + + while True: + time.sleep(30) + + amqp.shutdown(True) diff --git a/tests/test_basics.py b/tests/test_basics.py deleted file mode 100644 index b0051c672514ac182a59d92607b706d6382d9a39..0000000000000000000000000000000000000000 --- a/tests/test_basics.py +++ /dev/null @@ -1,139 +0,0 @@ -# coding=UTF-8 -from __future__ import absolute_import, division, print_function -import six - -from coolamqp import Cluster, ClusterNode, Queue, MessageReceived, ConnectionUp, ConsumerCancelled, Message, Exchange - -from tests.utils import getamqp, CoolAMQPTestCase - -class TestThings(CoolAMQPTestCase): - INIT_AMQP = False - - def test_different_constructor_for_clusternode(self): - cn = ClusterNode(host='127.0.0.1', user='guest', password='guest', virtual_host='/') - amqp = Cluster([cn]) - amqp.start() - self.assertIsInstance(amqp.drain(1), ConnectionUp) - amqp.shutdown() - - -#todo discard on fail needs tests - -class TestBasics(CoolAMQPTestCase): - - def test_acknowledge(self): - myq = Queue('myqueue', exclusive=True) - - self.amqp.consume(myq) - self.amqp.send(Message(b'what the fuck'), '', routing_key='myqueue') - - p = self.drainTo(MessageReceived, 4) - self.assertEquals(p.message.body, b'what the fuck') - self.assertIsInstance(p.message.body, six.binary_type) - p.message.ack() - - self.assertIs(self.amqp.drain(wait=1), None) - - def test_send_bullshit(self): - self.assertRaises(TypeError, lambda: Message(u'what the fuck')) - - def test_send_nonobvious_bullshit(self): - self.assertEquals(Message(bytearray(b'what the fuck')).body, b'what the fuck') - - def test_nacknowledge(self): - myq = Queue('myqueue', exclusive=True) - - self.amqp.consume(myq) - self.amqp.send(Message(b'what the fuck'), '', routing_key='myqueue') - - p = self.drainTo(MessageReceived, 4) - self.assertEquals(p.message.body, b'what the fuck') - p.message.nack() - - p = self.drainTo(MessageReceived, 4) - self.assertEquals(p.message.body, b'what the fuck') - - def test_bug_hangs(self): - p = Queue('lol', exclusive=True) - self.amqp.consume(p) - self.amqp.consume(p).result() - - def test_consume_declare(self): - """Spawn a second connection. One declares an exclusive queue, other tries to consume from it""" - with self.new_amqp_connection() as amqp2: - - has_failed = {'has_failed': False} - - self.amqp.declare_queue(Queue('lol', exclusive=True)).result() - amqp2.consume(Queue('lol', exclusive=True), on_failed=lambda e: has_failed.update({'has_failed': True})).result() - - self.assertTrue(has_failed['has_failed']) - - def test_qos(self): - self.amqp.qos(0, 1) - - self.amqp.consume(Queue('lol', exclusive=True)).result() - self.amqp.send(Message(b'what the fuck'), '', routing_key='lol') - self.amqp.send(Message(b'what the fuck'), '', routing_key='lol') - - p = self.drainTo(MessageReceived, 4) - - self.drainToNone(5) - p.message.ack() - self.assertIsInstance(self.amqp.drain(wait=4), MessageReceived) - - def test_consume_twice(self): - """Spawn a second connection and try to consume an exclusive queue twice""" - with self.new_amqp_connection() as amqp2: - - has_failed = {'has_failed': False} - - self.amqp.consume(Queue('lol', exclusive=True)).result() - amqp2.consume(Queue('lol', exclusive=True), on_failed=lambda e: has_failed.update({'has_failed': True})).result() - - self.assertTrue(has_failed['has_failed']) - - def test_send_and_receive(self): - myq = Queue('myqueue', exclusive=True) - - self.amqp.consume(myq) - self.amqp.send(Message(b'what the fuck'), '', routing_key='myqueue') - - self.assertEquals(self.drainTo(MessageReceived, 3).message.body, b'what the fuck') - - def test_consumer_cancelled_on_queue_deletion(self): - myq = Queue('myqueue', exclusive=True) - - self.amqp.consume(myq) - self.amqp.delete_queue(myq) - - self.assertEquals(self.drainTo(ConsumerCancelled, 5).reason, ConsumerCancelled.BROKER_CANCEL) - - def test_consumer_cancelled_on_consumer_cancel(self): - myq = Queue('myqueue', exclusive=True) - - self.amqp.consume(myq) - self.amqp.cancel(myq) - - c = self.drainTo(ConsumerCancelled, 10) - self.assertEquals(c.reason, ConsumerCancelled.USER_CANCEL) - - def test_delete_exchange(self): - xchg = Exchange('a_fanout', type='fanout') - self.amqp.declare_exchange(xchg) - self.amqp.delete_exchange(xchg).result() - - def test_exchanges(self): - xchg = Exchange('a_fanout', type='fanout') - self.amqp.declare_exchange(xchg) - - q1 = Queue('q1', exclusive=True, exchange=xchg) - q2 = Queue('q2', exclusive=True, exchange=xchg) - - self.amqp.consume(q1) - self.amqp.consume(q2) - - self.amqp.send(Message(b'hello'), xchg) - - self.drainTo(MessageReceived, 4) - self.drainTo(MessageReceived, 4) diff --git a/tests/test_connection/__init__.py b/tests/test_connection/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..9f2b35b38d89264ee25685611d0a65a192e165f6 --- /dev/null +++ b/tests/test_connection/__init__.py @@ -0,0 +1,2 @@ +# coding=UTF-8 +from __future__ import absolute_import, division, print_function diff --git a/tests/test_connection/test_state.py b/tests/test_connection/test_state.py new file mode 100644 index 0000000000000000000000000000000000000000..2d36b98a966d3ddb9cd1ba071ed8175385ebf222 --- /dev/null +++ b/tests/test_connection/test_state.py @@ -0,0 +1,36 @@ +# coding=UTF-8 +from __future__ import absolute_import, division, print_function +import unittest + + +from coolamqp.connection.state import Broker +from coolamqp.objects import NodeDefinition +from coolamqp.uplink import ListenerThread, Connection, Handshaker +import socket +import time + + +def newc(): + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.connect(('127.0.0.1', 5672)) + s.settimeout(0) + s.send('AMQP\x00\x00\x09\x01') + return s + + +NODE = NodeDefinition('127.0.0.1', 5672, 'user', 'user') + + +class TestState(unittest.TestCase): + def test_basic(self): + lt = ListenerThread() + lt.start() + + broker = Broker(Connection(newc(), lt), NODE) + + ord = broker.connect() + ord.wait() + + time.sleep(100) + + lt.terminate() diff --git a/tests/test_failures.py b/tests/test_failures.py deleted file mode 100644 index c658026279a85db9039718dbb51a76173ff1cbf1..0000000000000000000000000000000000000000 --- a/tests/test_failures.py +++ /dev/null @@ -1,146 +0,0 @@ -# coding=UTF-8 -from __future__ import absolute_import, division, print_function - -import unittest -import time -from coolamqp import Cluster, ClusterNode, Queue, MessageReceived, ConnectionUp, \ - ConnectionDown, ConsumerCancelled, Message, Exchange - - -NODE = ClusterNode('127.0.0.1', 'guest', 'guest') - -from tests.utils import CoolAMQPTestCase - - -class TestSpecialCases(CoolAMQPTestCase): - INIT_AMQP = False - - def test_termination_while_disconnect(self): - self.amqp = Cluster([NODE]) - self.amqp.start() - self.assertIsInstance(self.amqp.drain(wait=1), ConnectionUp) - - self.fail_amqp() - time.sleep(5) - self.assertIsInstance(self.amqp.drain(wait=1), ConnectionDown) - - self.amqp.shutdown() - self.assertIsNone(self.amqp.thread.backend) - self.assertFalse(self.amqp.connected) - - self.unfail_amqp() - - -class TestFailures(CoolAMQPTestCase): - - def test_cancel_not_consumed_queue(self): - self.amqp.cancel(Queue('hello world')).result() - - def test_longer_disconnects(self): - self.fail_amqp() - time.sleep(3) - self.drainTo(ConnectionDown, 4) - time.sleep(12) - self.unfail_amqp() - self.drainTo(ConnectionUp, 35) - - def test_qos_redeclared_on_fail(self): - self.amqp.qos(0, 1).result() - - self.restart_rmq() - - self.amqp.consume(Queue('lol', exclusive=True)).result() - self.amqp.send(Message(b'what the fuck'), '', routing_key='lol') - self.amqp.send(Message(b'what the fuck'), '', routing_key='lol') - - p = self.drainTo(MessageReceived, 4) - - self.drainToNone(5) - p.message.ack() - self.assertIsInstance(self.amqp.drain(wait=4), MessageReceived) - - def test_connection_flags_are_okay(self): - self.fail_amqp() - self.drainTo(ConnectionDown, 8) - self.assertFalse(self.amqp.connected) - self.unfail_amqp() - self.drainTo(ConnectionUp, 5) - self.assertTrue(self.amqp.connected) - - def test_sending_a_message_is_cancelled(self): - """are messages generated at all? does it reconnect?""" - - self.amqp.consume(Queue('wtf1', exclusive=True)) - - self.fail_amqp() - self.drainTo(ConnectionDown, 5) - - p = self.amqp.send(Message(b'what the fuck'), routing_key='wtf1') - p.cancel() - self.assertTrue(p.wait()) - self.assertFalse(p.has_failed()) - - self.fail_unamqp() - self.drainToAny([ConnectionUp], 30, forbidden=[MessageReceived]) - - def test_qos_after_failure(self): - self.amqp.qos(0, 1) - - self.amqp.consume(Queue('lol', exclusive=True)).result() - self.amqp.send(Message(b'what the fuck'), '', routing_key='lol') - self.amqp.send(Message(b'what the fuck'), '', routing_key='lol') - - p = self.drainTo(MessageReceived, 4) - - self.assertIsNone(self.amqp.drain(wait=5)) - p.message.ack() - self.drainTo(MessageReceived, 4) - - self.restart_rmq() - - self.amqp.send(Message(b'what the fuck'), '', routing_key='lol') - self.amqp.send(Message(b'what the fuck'), '', routing_key='lol') - - p = self.drainTo(MessageReceived, 4) - - self.drainToNone(5) - p.message.ack() - self.drainTo(MessageReceived, 4) - - def test_connection_down_and_up_redeclare_queues(self): - """are messages generated at all? does it reconnect?""" - q1 = Queue('wtf1', exclusive=True, auto_delete=True) - self.amqp.consume(q1).result() - - self.restart_rmq() - - self.amqp.send(Message(b'what the fuck'), routing_key='wtf1') - - self.drainTo(MessageReceived, 10) - - def test_exchanges_are_redeclared(self): - xchg = Exchange('a_fanout', type='fanout') - self.amqp.declare_exchange(xchg) - - q1 = Queue('q1', exclusive=True, exchange=xchg) - q2 = Queue('q2', exclusive=True, exchange=xchg) - - self.amqp.consume(q1) - self.amqp.consume(q2).result() - - self.restart_rmq() - - self.amqp.send(Message(b'hello'), xchg) - self.drainTo([MessageReceived, MessageReceived], 20) - - def test_consuming_exclusive_queue(self): - # declare and eat - q1 = Queue('q1', exclusive=True) - - self.amqp.consume(q1).wait() - - with self.new_amqp_connection() as amqp2: - q2 = Queue('q1', exclusive=True) - - r = amqp2.consume(q2) - self.assertFalse(r.wait()) diff --git a/tests/test_framing/__init__.py b/tests/test_framing/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..9f2b35b38d89264ee25685611d0a65a192e165f6 --- /dev/null +++ b/tests/test_framing/__init__.py @@ -0,0 +1,2 @@ +# coding=UTF-8 +from __future__ import absolute_import, division, print_function diff --git a/tests/test_framing/test_definitions/__init__.py b/tests/test_framing/test_definitions/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..9f2b35b38d89264ee25685611d0a65a192e165f6 --- /dev/null +++ b/tests/test_framing/test_definitions/__init__.py @@ -0,0 +1,2 @@ +# coding=UTF-8 +from __future__ import absolute_import, division, print_function diff --git a/tests/test_framing/test_definitions/test_cpl.py b/tests/test_framing/test_definitions/test_cpl.py new file mode 100644 index 0000000000000000000000000000000000000000..03b8e9f12830f200aa25a6cc9823acb856ee4acf --- /dev/null +++ b/tests/test_framing/test_definitions/test_cpl.py @@ -0,0 +1,32 @@ +# coding=UTF-8 +from __future__ import absolute_import, division, print_function +import unittest +import io + + +from coolamqp.framing.definitions import BasicContentPropertyList + + +class TestBasicContentPropertyList(unittest.TestCase): + def test_bcpl1(self): + bcpl = BasicContentPropertyList(content_type='text/plain', content_encoding='utf8') + + self.assertEquals(bcpl.content_type, 'text/plain') + self.assertEquals(bcpl.content_encoding, 'utf8') + + buf = io.BytesIO() + bcpl.write_to(buf) + + ser = buf.getvalue() + self.assertEquals(ser, '\xC0\x00' + chr(len('text/plain')) + b'text/plain\x04utf8') + + def test_bcpl2(self): + bcpl = BasicContentPropertyList(content_type='text/plain') + + self.assertEquals(bcpl.content_type, 'text/plain') + + buf = io.BytesIO() + bcpl.write_to(buf) + + ser = buf.getvalue() + self.assertEquals(ser, '\x80\x00' + chr(len('text/plain')) + b'text/plain') diff --git a/tests/test_framing/test_definitions/test_frames.py b/tests/test_framing/test_definitions/test_frames.py new file mode 100644 index 0000000000000000000000000000000000000000..c805647442bd32414d4d2c0bc9a412b1fead5f06 --- /dev/null +++ b/tests/test_framing/test_definitions/test_frames.py @@ -0,0 +1,46 @@ +# coding=UTF-8 +from __future__ import absolute_import, division, print_function +import unittest +import io +import struct +from coolamqp.framing.frames import AMQPHeaderFrame +from coolamqp.framing.definitions import BasicContentPropertyList, FRAME_HEADER, FRAME_END, ConnectionStartOk + + +class TestShitSerializesRight(unittest.TestCase): + + def test_unser_header_frame(self): + s = b'\x00\x3C\x00\x00' + \ + b'\x00\x00\x00\x00\x00\x00\x00\x0A' + \ + b'\xC0\x00\x0Atext/plain\x04utf8' + + hf = AMQPHeaderFrame.unserialize(0, buffer(s)) + + self.assertEquals(hf.class_id, 60) + self.assertEquals(hf.weight, 0) + self.assertEquals(hf.body_size, 10) + self.assertEquals(hf.properties.content_type, b'text/plain') + self.assertEquals(hf.properties.content_encoding, b'utf8') + + def test_ser_header_frame(self): + + a_cpl = BasicContentPropertyList(content_type='text/plain') + + # content_type has len 10 + + buf = io.BytesIO() + + hdr = AMQPHeaderFrame(0, 60, 0, 0, a_cpl) + hdr.write_to(buf) + + s = b'\x00\x00\x00\x00' + \ + b'\x00\x00\x00\x00\x00\x00\x00\x00' + \ + b'\x80\x00\x0Atext/plain' + s = chr(FRAME_HEADER) + b'\x00\x00' + \ + struct.pack('!L', len(s)) + s + chr(FRAME_END) + + self.assertEquals(buf.getvalue(), + + ) + + diff --git a/tests/test_framing/test_field_table.py b/tests/test_framing/test_field_table.py new file mode 100644 index 0000000000000000000000000000000000000000..0f3f7ccbb82889b6f48322f10bda69aaa916a499 --- /dev/null +++ b/tests/test_framing/test_field_table.py @@ -0,0 +1,46 @@ +# coding=UTF-8 +from __future__ import absolute_import, division, print_function +import unittest +import struct +import io + +from coolamqp.framing.field_table import enframe_table, deframe_table, frame_table_size, \ + enframe_field_value, deframe_field_value, frame_field_value_size + + +class TestFramingTables(unittest.TestCase): + def test_frame_unframe_table(self): + + tab = [ + (b'field', (b'yo', b's')) + ] + + buf = io.BytesIO() + + self.assertEquals(frame_table_size(tab), 4+6+4) + + enframe_table(buf, tab) + buf = buf.getvalue() + + self.assertEquals(buf, struct.pack('!I', 10) + b'\x05fields\x02yo') + + tab, delta = deframe_table(buffer(buf), 0) + + self.assertEquals(len(tab), 1) + self.assertEquals(delta, 14) + self.assertEquals(tab[0], (b'field', (b'yo', b's'))) + + def test_frame_unframe_value(self): + + buf = io.BytesIO() + + enframe_field_value(buf, (b'yo', b's')) + + buf = buf.getvalue() + self.assertEquals(b's\x02yo', buf) + + fv, delta = deframe_field_value(buffer(buf), 0) + self.assertEquals(fv, (b'yo', b's')) + self.assertEquals(delta, 4) + + self.assertEquals(frame_field_value_size(fv), 4) diff --git a/tests/test_noack.py b/tests/test_noack.py deleted file mode 100644 index 2720947bb85057fbbaf9dd90a755232438e682d8..0000000000000000000000000000000000000000 --- a/tests/test_noack.py +++ /dev/null @@ -1,109 +0,0 @@ -# coding=UTF-8 -from __future__ import absolute_import, division, print_function -import six -import unittest -import time - -from tests.utils import CoolAMQPTestCase -from coolamqp import Cluster, ClusterNode, Queue, MessageReceived, ConnectionUp, ConnectionDown, ConsumerCancelled, Message, Exchange - -class TestNoAcknowledge(CoolAMQPTestCase): - def test_noack_works(self): - myq = Queue('myqueue', exclusive=True) - - self.amqp.qos(0, 1, False) - - self.amqp.consume(myq, no_ack=True) - - self.amqp.send(Message(b'what the fuck'), '', routing_key='myqueue') - self.amqp.send(Message(b'what the fuck'), '', routing_key='myqueue') - self.amqp.send(Message(b'what the fuck'), '', routing_key='myqueue') - - self.drainTo([MessageReceived, MessageReceived, MessageReceived], [3, 3, 3]) - - def test_noack_works_after_restart(self): - myq = Queue('myqueue', exclusive=True) - - self.amqp.qos(0, 1, False) - - self.amqp.consume(myq, no_ack=True) - - self.amqp.send(Message(b'what the fuck'), '', routing_key='myqueue') - self.amqp.send(Message(b'what the fuck'), '', routing_key='myqueue') - self.amqp.send(Message(b'what the fuck'), '', routing_key='myqueue') - - self.drainTo([MessageReceived, MessageReceived, MessageReceived], [3, 3, 3]) - - self.restart_rmq() - - self.amqp.send(Message(b'what the fuck'), routing_key='myqueue') - self.amqp.send(Message(b'what the fuck'), routing_key='myqueue') - self.amqp.send(Message(b'what the fuck'), routing_key='myqueue') - - self.drainTo([MessageReceived, MessageReceived, MessageReceived], [3, 3, 3]) - - def test_noack_coexists(self): - self.amqp.qos(0, 1, False) - - self.amqp.consume(Queue('myqueue', exclusive=True), no_ack=True) - self.amqp.consume(Queue('myqueue2', exclusive=True)) - - msg = Message(b'zz') - - for i in range(3): - self.amqp.send(msg, routing_key='myqueue') - self.amqp.send(msg, routing_key='myqueue2') - - mq2s = [] - for i in range(4): - # I should have received 3 messages from myqueue, and 2 from myqueue2 - print('beng') - mer = self.drainTo(MessageReceived) - if mer.message.routing_key == 'myqueue2': - mq2s.append(mer.message) - - # Should receive nothing, since not acked - self.assertIsNone(self.amqp.drain(wait=4)) - - self.assertEquals(len(mq2s), 1) - - # ack and receive - for me in mq2s: me.ack() - self.drainTo(MessageReceived, 1).message.ack() # 2nd - self.drainTo(MessageReceived, 1).message.ack() # 3rd - - @unittest.skip('demonstrates a py-amqp bug') - def test_noack_coexists_empty_message_body(self): - self.amqp.qos(0, 1, False) - - self.amqp.consume(Queue('myqueue', exclusive=True), no_ack=True) - self.amqp.consume(Queue('myqueue2', exclusive=True)) - - msg = Message(b'') # if this is empty, py-amqp fails - - for i in range(3): - self.amqp.send(msg, routing_key='myqueue') - self.amqp.send(msg, routing_key='myqueue2') - - # And here connection with the broker snaps ..... - - mq2s = [] - for i in range(4): - # I should have received 3 messages from myqueue, and 2 from myqueue2 - mer = self.drainTo(MessageReceived) - if mer.message.routing_key == 'myqueue2': - mq2s.append(mer.message) - - # Should receive nothing, since not acked - self.assertIsNone(self.amqp.drain(wait=4)) - - self.assertEquals(len(mq2s), 1) - - # ack and receive - for me in mq2s: me.ack() - mer = self.amqp.drain(wait=1) # 2nd - self.assertIsInstance(mer, MessageReceived) - mer.message.ack() - mer = self.amqp.drain(wait=1) # 3rd - self.assertIsInstance(mer, MessageReceived) - mer.message.ack() \ No newline at end of file diff --git a/tests/test_performance.py b/tests/test_performance.py deleted file mode 100644 index d5bc651ccd7b8f0c9380f450ec0e783111cadc8b..0000000000000000000000000000000000000000 --- a/tests/test_performance.py +++ /dev/null @@ -1,24 +0,0 @@ -# coding=UTF-8 -from __future__ import absolute_import, division, print_function -from tests.utils import CoolAMQPTestCase -import six -import time - -from coolamqp import Cluster, ClusterNode, Queue, MessageReceived, ConnectionUp, \ - ConnectionDown, ConsumerCancelled, Message, Exchange - - -class TestBasics(CoolAMQPTestCase): - def setUp(self): - self.amqp = Cluster([ClusterNode('127.0.0.1', 'guest', 'guest')]) - self.amqp.start() - self.assertIsInstance(self.amqp.drain(1), ConnectionUp) - - def tearDown(self): - self.amqp.shutdown() - - def test_sending_a_message(self): - - with self.takes_less_than(0.5): - self.amqp.send(Message(b''), routing_key='nowhere').result() - diff --git a/tests/test_uplink/__init__.py b/tests/test_uplink/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..9f2b35b38d89264ee25685611d0a65a192e165f6 --- /dev/null +++ b/tests/test_uplink/__init__.py @@ -0,0 +1,2 @@ +# coding=UTF-8 +from __future__ import absolute_import, division, print_function diff --git a/tests/test_uplink/test_basic.py b/tests/test_uplink/test_basic.py new file mode 100644 index 0000000000000000000000000000000000000000..034d7e9aad28a57f19e70a347a4e5254df5a12ef --- /dev/null +++ b/tests/test_uplink/test_basic.py @@ -0,0 +1,57 @@ +# coding=UTF-8 +from __future__ import absolute_import, division, print_function +import unittest + +from coolamqp.uplink import ListenerThread, Connection, Handshaker +import socket +import time + + +def newc(): + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.connect(('127.0.0.1', 5672)) + s.settimeout(0) + s.send('AMQP\x00\x00\x09\x01') + return s + + +class TestBasic(unittest.TestCase): + def test_gets_connectionstart(self): + + hnd_ok = {'ok': False} + def hnd_suc(): + hnd_ok['ok'] = True + + lt = ListenerThread() + lt.start() + + con = Connection(newc(), lt) + + Handshaker(con, 'user', 'user', '/', hnd_suc, lambda: None) + con.start() + + time.sleep(5) + + lt.terminate() + self.assertTrue(hnd_ok['ok']) + + + def test_heartbeats(self): + + hnd_ok = {'ok': False} + def hnd_suc(): + hnd_ok['ok'] = True + + lt = ListenerThread() + lt.start() + + con = Connection(newc(), lt) + + Handshaker(con, 'user', 'user', '/', hnd_suc, lambda: None, 3) + con.start() + + time.sleep(20) + + self.assertFalse(con.failed) + lt.terminate() + self.assertTrue(hnd_ok['ok'])