amqp-1.8.0/0000755000004100000410000000000013321132064012501 5ustar www-datawww-dataamqp-1.8.0/docs/0000755000004100000410000000000013321132064013431 5ustar www-datawww-dataamqp-1.8.0/docs/Clustering.textile0000644000004100000410000000172113321132064017151 0ustar www-datawww-data# @title Ruby AMQP gem: Clustering h1. Clustering h2. This Documentation Has Moved to rubyamqp.info amqp gem documentation guides are now hosted on "rubyamqp.info":http://rubyamqp.info. h2. About this guide This guide covers clustered AMQP broker setups, with RabbitMQ as example. h2. Covered versions This guide covers "Ruby amqp gem":http://github.com/ruby-amqp/amqp v0.9.0 and later. TBD h2. Clustering TBD h2. Tell us what you think! Please take a moment and tell us what you think about this guide "on Twitter":http://twitter.com/rubyamqp or "Ruby AMQP mailing list":http://groups.google.com/group/ruby-amqp: what was unclear? what wasn't covered? maybe you don't like guide style or grammar and spelling are incorrect? Readers feedback is key to making documentation better. If mailing list communication is not an option for you for some reason, you can "contact guides author directly":mailto:michael@novemberain.com?subject=amqp%20gem%20documentation amqp-1.8.0/docs/ConnectionEncryptionWithTLS.textile0000644000004100000410000000652013321132064022425 0ustar www-datawww-data# @title Ruby AMQP gem: Using TLS h1. Using TLS with Ruby amqp gem h2. This Documentation Has Moved to rubyamqp.info amqp gem documentation guides are now hosted on "rubyamqp.info":http://rubyamqp.info. h2. About this guide This guide covers connection to AMQP brokers using TLS (also known as SSL) and related issues. This guide does not explain basic TLS concepts. For that, refer to resources like "Introduction to SSL":https://developer.mozilla.org/en/Introduction_to_SSL or "Wikipedia page on TLS":http://en.wikipedia.org/wiki/Transport_Layer_Security. h2. Covered versions This guide covers "Ruby amqp gem":http://github.com/ruby-amqp/amqp v0.8.0 and later. h2. Broker version requirements h3. RabbitMQ RabbitMQ supports TLS since version 1.7.0. Minimum requirements are * Erlang R13B * Erlang SSL application 3.10 The recommended distribution is R14B (SSL 4.0.1) or later. This should be considered the minimum configuration for Java and Erlang clients due to an incorrect RC4 implementation in earlier versions of Erlang. Learn more at "rabbitmq.com TLS page":http://www.rabbitmq.com/ssl.html. h2. Pre-requisites AMQP brokers typically need to be configured to use TLS. Just like Web servers, TLS connections are usually accepted on a separate port (5671). "rabbitmq.com TLS page":http://www.rabbitmq.com/ssl.html describes how to configure RabbitMQ to use TLS, how to generate certificates for development and so on. h2. Connecting to AMQP broker using TLS To instruct Ruby amqp gem to use TLS for connection, pass :ssl option that specifies certificate chain file path as well as private key file path:

AMQP.start(:port     => 5671,
           :ssl => {
             :cert_chain_file  => certificate_chain_file_path,
             :private_key_file => client_private_key_file_path
           }) do |connection|
  puts "Connected, authenticated. TLS seems to work."

  connection.disconnect { puts "Now closing the connection…"; EventMachine.stop }
end

Note that TLS connection may take a bit of time to establish (up to several seconds in some cases). To verify that broker connection actually uses TLS, refer to RabbitMQ log file:
=INFO REPORT==== 28-Jun-2011::08:41:24 ===
accepted TCP connection on 0.0.0.0:5671 from 127.0.0.1:53444

=INFO REPORT==== 28-Jun-2011::08:41:24 ===
starting TCP connection <0.9904.0> from 127.0.0.1:53444

=INFO REPORT==== 28-Jun-2011::08:41:24 ===
upgraded TCP connection <0.9904.0> to SSL
h2. Example code TLS example (as well as sample certificates you can use to get started with) can be found in the "amqp gem git repository":https://github.com/ruby-amqp/amqp/tree/master/examples h2. Authors This guide was written by "Michael Klishin":http://twitter.com/michaelklishin and edited by "Chris Duncan":https://twitter.com/celldee. h2. Tell us what you think! Please take a moment and tell us what you think about this guide "on Twitter":http://twitter.com/rubyamqp or "Ruby AMQP mailing list":http://groups.google.com/group/ruby-amqp: what was unclear? what wasn't covered? maybe you don't like guide style or grammar and spelling are incorrect? Readers feedback is key to making documentation better. If mailing list communication is not an option for you for some reason, you can "contact guides author directly":mailto:michael@novemberain.com?subject=amqp%20gem%20documentation amqp-1.8.0/docs/VendorSpecificExtensions.textile0000644000004100000410000001731213321132064022020 0ustar www-datawww-data# @title Ruby AMQP gem: Using vendor-specific AMQP extensions h1. Vendor-specific AMQP extensions support in amqp gem h2. This Documentation Has Moved to rubyamqp.info amqp gem documentation guides are now hosted on "rubyamqp.info":http://rubyamqp.info. h2. RabbitMQ extensions h2. Supported extensions AMQP gem supports many RabbitMQ extensions to AMQP 0.9.1: * "Publisher confirmations":http://www.rabbitmq.com/extensions.html#confirms (confirm.* class) * "Negative acknowledgements":http://www.rabbitmq.com/extensions.html#negative-acknowledgements (basic.nack) * "Alternate Exchanges":http://www.rabbitmq.com/extensions.html#alternate-exchange * "Per-queue Message Time-to-Live":http://www.rabbitmq.com/extensions.html#queue-ttl * "Queue Leases":http://www.rabbitmq.com/extensions.html#queue-leases * "Sender-selected Distribution":http://www.rabbitmq.com/extensions.html#sender-selected-distribution * "Validated user_id":http://www.rabbitmq.com/extensions.html#validated-user-id h2. Enabling RabbitMQ extensions If you are using RabbitMQ as AMQP broker and want to use these extensions, simply replace
require "amqp"
with
require "amqp"
require "amqp/extensions/rabbitmq"
h2. Per-queue Message Time-to-Live Per-queue Message Time-to-Live (TTL) is a RabbitMQ extension to AMQP 0.9.1 that lets developers control for how long a message published to a queue can live before it is discarded. A message that has been in the queue for longer than the configured TTL is said to be dead. Dead messages will not be delivered to consumers and cannot be fetched using *basic.get* operation ({AMQP::Queue#pop}). Message TTL is specified using *x-message-ttl* argument on declaration. With amqp gem, you pass it to {AMQP::Queue#initialize} or {AMQP::Channel#queue}:

# 1000 milliseconds
channel.queue("", :arguments => { "x-message-ttl" => 1000 })

When a published messages is routed to multiple queues, each of the queues gets a _copy of the message_. If then the message dies in one of the queues, it has no effect on copies of the message in other queues. h3. Example The example below sets message TTL for a new server-named queue to be 1000 milliseconds. It then publishes several messages that are routed to the queue and tries to fetch messages using *basic.get* AMQP method ({AMQP::Queue#pop} after 0.7 and 1.5 seconds: h3. Learn More See also rabbitmq.com section on "Per-queue Message TTL":http://www.rabbitmq.com/extensions.html#queue-ttl h2. Publisher Confirms (Publisher Acknowledgements) In some situations not a single message can be lost. The only reliable way of doing so is using confirmations. "Publisher Confirms AMQP extension":http://www.rabbitmq.com/blog/2011/02/10/introducing-publisher-confirms/ was designed to solve the reliable publishing problem. Publisher confirms are similar to message acknowledgements documented in the {file:docs/Queues.textile Working With Queues} guide but involve publisher and AMQP broker instead of consumer and AMQP broker. !https://github.com/ruby-amqp/amqp/raw/master/docs/diagrams/006_amqp_091_message_acknowledgements.png! !https://github.com/ruby-amqp/amqp/raw/master/docs/diagrams/007_rabbitmq_publisher_confirms.png! h3. Public API To use publisher confirmations, first put channel into confirmation mode using {AMQP::Channel#confirm_select}:

channel.confirm_select

From this moment on, every message published on this channel will cause channel's _publisher index_ (message counter) to be incremented. It is possible to access using {AMQP::Channel#publisher_index} method. To check whether channel is in the confirmation mode, use {AMQP::Channel#uses_publisher_confirmations?} predicate. To handle AMQP broker acknowledgements, define a handler using {AMQP::Channel#on_ack}, for example:

channel.on_ack do |basic_ack|
 puts "Received basic_ack: multiple = #{basic_ack.multiple}, delivery_tag = #{basic_ack.delivery_tag}"
end

Delivery tag will indicate number of confirmed messages. If *multiple* attribute is true, the confirmation is for all messages up to the number delivery tag indicates. In other words, AMQP broker may confirm just one message or a batch of them. h3. Example h3. Learn More See also rabbitmq.com section on "Confirms aka Publisher Acknowledgements":http://www.rabbitmq.com/extensions.html#confirms h2. basic.nack The AMQP specification defines the basic.reject method that allows clients to reject individual, delivered messages, instructing the broker to either discard them or requeue them. Unfortunately, basic.reject provides no support for negatively acknowledging messages in bulk. To solve this, RabbitMQ supports the basic.nack method that provides all the functionality of basic.reject whilst also allowing for bulk processing of messages. h3. Public API When RabbitMQ extensions are loaded, {AMQP::Channel#reject} method is overriden via mixin to take one additional argument: multi (defaults to false). When it is given and is true, amqp gem will use basic.nack AMQP method instead of basic.reject, to reject multiple messages at once. Otherwise, basic.reject is used as usual. h3. Learn More See also rabbitmq.com section on "Confirms aka Publisher Acknowledgements":http://www.rabbitmq.com/extensions.html#negative-acknowledgements h2. Alternate Exchanges Alternate Exchanges is a RabbitMQ extension to AMQP 0.9.1 that lets developers define "fallback" exchanges where unroutable messages will be sent. h3. Public API To specify exchange A as alternate exchange to exchange B, specify 'alternate-exchange' argument on declaration of B:

exchange1 = channel.fanout("ops.fallback",     :auto_delete => true)
exchange2 = channel.fanout("events.collector", :auto_delete => true, :arguments => { "alternate-exchange" => "ops.fallback" })

h3. Example h3. Learn More See also rabbitmq.com section on "Alternate Exchanges":http://www.rabbitmq.com/extensions.html#alternate-exchange h2. Authors This guide was written by "Michael Klishin":http://twitter.com/michaelklishin and edited by "Chris Duncan":https://twitter.com/celldee. h2. Tell us what you think! Please take a moment and tell us what you think about this guide "on Twitter":http://twitter.com/rubyamqp or "Ruby AMQP mailing list":http://groups.google.com/group/ruby-amqp: what was unclear? what wasn't covered? maybe you don't like guide style or grammar and spelling are incorrect? Readers feedback is key to making documentation better. If mailing list communication is not an option for you for some reason, you can "contact guides author directly":mailto:michael@novemberain.com?subject=amqp%20gem%20documentation
amqp-1.8.0/docs/RabbitMQVersions.textile0000644000004100000410000000774213321132064020235 0ustar www-datawww-data# @title Ruby AMQP gem: RabbitMQ versions compatibility h1. Ruby amqp gem and RabbitMQ versions compatibility h2. This Documentation Has Moved to rubyamqp.info amqp gem documentation guides are now hosted on "rubyamqp.info":http://rubyamqp.info. h2. About this guide This guide covers compatibility of the "Ruby amqp gem":http://github.com/ruby-amqp/amqp with various versions of "RabbitMQ":http://rabbitmq.com messaging broker. h2. Covered versions This guide covers "Ruby amqp gem":http://github.com/ruby-amqp/amqp v0.8.0 and later. h2. RabbitMQ Version Requirement amqp gem before 0.8.0 (0.6.x, 0.7.x) series implemented (most of) AMQP 0.8 specification. amqp gem 0.8.0 implements AMQP 0.9.1 and thus *requires RabbitMQ version 2.0 or later*. See {file:docs/RabbitMQVersions.textile RabbitMQ versions} for more information about RabbitMQ versions support and how obtain up-todate packages for your operating system. amqp gem 0.8.0 and later versions implement AMQP 0.9.1 and thus *requires RabbitMQ version 2.0 or later* h2. Using recent versions on Debian and Ubuntu Ubuntu (even 10.10) and Debian both "ship with an old RabbitMQ version":http://packages.ubuntu.com/maverick/rabbitmq-server, that only supports AMQP protocol 0.8. Ruby amqp gem 0.8.0 and later *will not work with RabbitMQ versions before 2.0.0*. We strongly recommend that you use "RabbitMQ apt repository":http://www.rabbitmq.com/debian.html#apt that has recent versions of RabbitMQ. h2. OpsCode Chef & Puppet h3. Chef cookbook for RabbitMQ There is a "Chef cookbook for RabbitMQ":https://github.com/opscode/cookbooks/tree/master/rabbitmq that installs recent versions from the rabbitmq.com apt repository. It also has LWPRs (providers) for managing users and vhosts. h3. RabbitMQ Puppet module There is a "RabbitMQ Puppet module":https://github.com/puppetlabs/puppetlabs-rabbitmq by the Puppet Labs team. It uses .deb packages from Debian testing and unstable repositories. Note that it has two dependencies: * "puppet-stdlib":https://github.com/puppetlabs/puppetlabs-stdlib * "puppet-apt":https://github.com/puppetlabs/puppet-apt h2. TLS (SSL) support Note that "before 1.7.0, RabbitMQ did not support TLS":http://www.rabbitmq.com/ssl.html. In order to have TLS support, RabbitMQ 1.7.0 requires * Erlang/OTP R13B or later * Erlang SSL 3.10 or later and recommends using Erlang R141B that ships with Erlang SSL 4.0.1. Learn more in our {file:docs/ConnectionEncryptionWithTLS.textile Using TLS (SSL)} guide. h2. Authors This guide was written by "Michael Klishin":http://twitter.com/michaelklishin and edited by "Chris Duncan":https://twitter.com/celldee. h2. Tell us what you think! Please take a moment and tell us what you think about this guide "on Twitter":http://twitter.com/rubyamqp or "Ruby AMQP mailing list":http://groups.google.com/group/ruby-amqp: what was unclear? what wasn't covered? maybe you don't like guide style or grammar and spelling are incorrect? Readers feedback is key to making documentation better. If mailing list communication is not an option for you for some reason, you can "contact guides author directly":mailto:michael@novemberain.com?subject=amqp%20gem%20documentation
amqp-1.8.0/docs/DocumentationGuidesIndex.textile0000644000004100000410000001736313321132064022005 0ustar www-datawww-data# @title Ruby AMQP gem documentation guides h1. Ruby AMQP gem documentation guides h2. This Documentation Has Moved to rubyamqp.info amqp gem documentation guides are now hosted on "rubyamqp.info":http://rubyamqp.info. h2. Which versions of the amqp gem do the guides cover? These guides cover v0.8.0.RC14 and later of the "Ruby amqp gem":http://github.com/ruby-amqp/amqp. h2. Documentation structure and how to read these guides We recommend that you read the documentation guides in the order that they are listed. The *Getting Started* guide is written as a tutorial that describes several typical applications and their problem scope, then provides full code examples and finally breaks them down into smaller pieces that are explained in detail. Once you are finished with the tutorial, reading the other guides in sequence will gradually explain all of the AMQP 0.9.1 features that are relevant to application developers, including application design concerns and common practices. At present, some guides are yet to be written but *most of the content is concentrated in just 3-4 guides* that are about 80% finished and provide plenty of examples. Here is a summary of guides and their content:
{file:docs/GettingStarted.textile Getting Started with Ruby amqp gem and RabbitMQ}
Walks you through gem installation and 3 applications that demonstrate what AMQP has to offer. Explains how amqp gem should be integrated into rich object-oriented Ruby programs.
{file:docs/AMQP091ModelExplained.textile AMQP 0.9.1 Model Explained}
A simple, 2 page long introduction to the AMQP 0.9.1 model, focusing on what purpose individual features have. Reading other documentation guides will be easier when you are armed with this knowledge: a lot of the AMQP & RabbitMQ power comes from the AMQP Model.
{file:docs/ConnectingToTheBroker.textile Connecting to the Broker}
Connecting to AMQP broker. How to integrate with standalone applications as well as Ruby on Rails, Merb, Sinatra or Rack apps. What difference application server (Unicorn, Passenger, Thin, etc) makes. How to handle authentication failures and network connectivity issues. Closing AMQP connection gracefully.
{file:docs/Queues.textile Working With Queues}
What AMQP queues are. How to declare AMQP queues. When and how to use server-named and explicitly named queues. How to subscribe for "push" message delivery. What message acknowledgements are. How to access AMQP message metadata. How to fetch ("pull") messages from queues on demand. How to bind and unbind a queue to an exchange. How to delete a queue.
{file:docs/Exchanges.textile Working With Exchanges}
What AMQP exchanges are. Concept of binding. How to declare AMQP exchanges. How different exchange types route messages and common use cases. How to publish messages, especially how to do it reliably. What AMQP transactions are. What Publisher Confirms are. When messages are returned. How to delete an exchange.
{file:docs/Bindings.textile Bindings}
What AMQP bindings are, in-depth. How to bind queues to exchanges. How to unbind queues from exchanges. What's the life cycle of messages is. How unroutable messages are handled.
{file:docs/PatternsAndUseCases.textile Patterns and Use Cases}
Typical use cases, patterns and routing topologies.
{file:docs/Durability.textile Durability and Message Persistence}
Exchange durability. Queue durability. Message persistence. Performance implications.
{file:docs/ErrorHandling.textile Error Handling and Recovery}
Network failures. Connection-level exceptions. Channel-level exceptions. Why error handling is easy but recovery is hard. How to survive typical problems. What other tools can help (e.g. HAProxy).
{file:docs/TestingWithEventedSpec.textile Unit and integration testing of AMQP applications}
Unit testing of asynchronous code: typical problems and ways to solve them. An oviewview of evented-spec, the gem that amqp gem itself uses for "its test suite":https://github.com/ruby-amqp/amqp/tree/master/spec.
{file:docs/RabbitMQVersions.textile RabbitMQ versions}
RabbitMQ versions that amqp gem supports. Popular Linux distributions and RabbitMQ versions they ship. How to obtain up-to-date official packages of RabbitMQ.
{file:docs/ConnectionEncryptionWithTLS.textile Using TLS (SSL)}
All things related to TLS-encrypted connections.
When more than one guide describes the same concept, we make sure to use cross-references, however, only one guide discusses the concept in detail. h2. Full guide list * {file:docs/GettingStarted.textile Getting Started with Ruby amqp gem and RabbitMQ} * {file:docs/AMQP091ModelExplained.textile AMQP 0.9.1 Model Explained} * {file:docs/ConnectingToTheBroker.textile Connecting to the Broker} * {file:docs/Queues.textile Working With Queues} * {file:docs/Exchanges.textile Working With Exchanges} * {file:docs/Bindings.textile Bindings} * {file:docs/PatternsAndUseCases.textile Patterns and Use Cases} * {file:docs/Durability.textile Durability and Message Persistence} * {file:docs/ErrorHandling.textile Error Handling and Recovery} * {file:docs/08Migration.textile Upgrading from version 0.6.x/0.7.x to 0.8.x and above} * {file:docs/TestingWithEventedSpec.textile Unit and integration testing of AMQP applications} * {file:docs/Troubleshooting.textile Troubleshooting and debugging AMQP applications} * {file:docs/Clustering.textile Clustering} * {file:docs/RabbitMQVersions.textile RabbitMQ versions} * {file:docs/ConnectionEncryptionWithTLS.textile Using TLS (SSL)} * {file:docs/VendorSpecificExtensions.textile Vendor-specific Extensions to AMQP 0.9.1 spec} * {file:docs/RunningTests.textile Running amqp gem test suite} h2. License This work is licensed under a Creative Commons Attribution 3.0 Unported License (including images & stylesheets). The source is available "on Github":https://github.com/ruby-amqp/amqp/tree/master/docs. h2. Authors These guides were written by "Michael Klishin":http://twitter.com/michaelklishin and edited by "Chris Duncan":https://twitter.com/celldee h2. Tell us what you think! Please take a moment to tell us what you think about this guide "on Twitter":http://twitter.com/rubyamqp or the "Ruby AMQP mailing list":http://groups.google.com/group/ruby-amqp. Let us know what was unclear or what has not been covered. Maybe you do not like the guide style or grammar or discover spelling mistakes. Reader feedback is key to making the documentation better. If, for some reason, you cannot use the communication channels mentioned above, you can "contact the author of the guides directly":mailto:michael@novemberain.com?subject=amqp%20gem%20documentation
amqp-1.8.0/docs/ConnectingToTheBroker.textile0000644000004100000410000004773613321132064021252 0ustar www-datawww-data# @title Ruby amqp gem: Connecting to the broker, integrating with Ruby on Rails, Merb and Sinatra h1. Connecting to the broker, integrating with Ruby on Rails, Merb and Sinatra h2. This Documentation Has Moved to rubyamqp.info amqp gem documentation guides are now hosted on "rubyamqp.info":http://rubyamqp.info. h2. About this guide This guide covers connection to an AMQP broker from standalone and Web applications, connection error handling, authentication failure handling and related issues. This work is licensed under a Creative Commons Attribution 3.0 Unported License (including images & stylesheets). The source is available "on Github":https://github.com/ruby-amqp/amqp/tree/master/docs. h2. Which versions of the amqp gem does this guide cover? This guide covers v0.8.0 and later of the "Ruby amqp gem":http://github.com/ruby-amqp/amqp. h2. Terminology In this guide we define a standalone application as an application that does not run on a Web server like Unicorn or Passenger. The key difference is that these applications control the main Ruby VM thread and often use it to run the EventMachine event loop. When the amqp gem is used in a Web application, the main thread is occupied by the Web application server and the code required to establish a connection to an AMQP broker needs to be a little bit different. h2. Two ways to specify connection parameters Connection parameters (host, port, username, vhost and so on) can be passed in two forms: * As a hash * As a connection URI string (à la JDBC) h3. Using a hash Hash options that the amqp gem will recognize are * :host * :port * :username (aliased as :user) * :password (aliased as :pass) * :vhost * :ssl * :timeout * :frame_max h4. Default parameters Default connection parameters are

{
  :host      => "127.0.0.1",
  :port      => 5672,
  :user      => "guest",
  :pass      => "guest",
  :vhost     => "/",
  :ssl       => false,
  :frame_max => 131072
}

h3. Using connection strings It is convenient to be able to specify the AMQP connection parameters as a URI string, and various "amqp" URI schemes exist. Unfortunately, there is no standard for these URIs, so while the schemes share the same basic idea, they differ in some details. This implementation aims to encourage URIs that work as widely as possible. Here are some examples: * amqp://dev.rabbitmq.com * amqp://dev.rabbitmq.com:5672 * amqp://guest:guest@dev.rabbitmq.com:5672 * amqp://hedgehog:t0ps3kr3t@hub.megacorp.internal/production * amqps://hub.megacorp.internal/%2Fvault The URI scheme should be "amqp", or "amqps" if SSL is required. The host, port, username and password are represented in the authority component of the URI in the same way as in http URIs. The vhost is obtained from the first segment of the path, with the leading slash removed. The path should contain only a single segment (i.e, the only slash in it should be the leading one). If the vhost is to include slashes or other reserved URI characters, these should be percent-escaped. Here are some examples that demonstrate how {AMQP::Client.parse_connection_uri} parses out the vhost from connection URIs:

AMQP::Client.parse_connection_uri("amqp://dev.rabbitmq.com")            # => vhost is nil, so default ("/") will be used
AMQP::Client.parse_connection_uri("amqp://dev.rabbitmq.com/")           # => vhost is an empty string
AMQP::Client.parse_connection_uri("amqp://dev.rabbitmq.com/%2Fvault")   # => vhost is "/vault"
AMQP::Client.parse_connection_uri("amqp://dev.rabbitmq.com/production") # => vhost is "production"
AMQP::Client.parse_connection_uri("amqp://dev.rabbitmq.com/a.b.c")      # => vhost is "a.b.c"
AMQP::Client.parse_connection_uri("amqp://dev.rabbitmq.com/foo/bar")  # => ArgumentError

h2. Starting the event loop and connecting in standalone applications h3. EventMachine event loop The amqp gem uses "EventMachine":http://rubyeventmachine.com under the hood and needs an EventMachine event loop to be running in order to connect to an AMQP broker or to send any data. This means that before connecting to an AMQP broker, we need to _start the EventMachine reactor_ (get the event loop going). Here is how to do it:

require "amqp"

EventMachine.run do
  # ...
end

"EventMachine.run":http://eventmachine.rubyforge.org/EventMachine.html#M000461 will block the current thread until the event loop is stopped. Standalone applications often can afford to start the event loop on the main thread. If you have no experience with threading, this is a recommended way to proceed. h3. Using AMQP.connect with a block Once the event loop is running, the {AMQP.connect} method will attempt to connect to the broker. It can be used in two ways. Here is the first one:

require "amqp"

EventMachine.run do
  # using AMQP.connect with a block
  AMQP.connect(:host => "localhost") do |client|
    # connection is open and ready to be used
  end
end

{AMQP.connect} takes a block that will be executed as soon as the AMQP connection is open. In order for a connection to be opened a TCP connection has to be set up, authentication has to succeed, and the broker and client need to complete negotiation of connection parameters like max frame size. h3. Using AMQP.connect without a callback An alternative way of connecting is this:

require "amqp"

EventMachine.run do
  # using AMQP.connect with a block
  client = AMQP.connect(:host => "hub.megacorp.internal", :username => "hedgehog", :password => "t0ps3kr3t")
  # connection is not yet open, however, amqp gem will delay channel
  # operations until after the connection is open. Bear in mind that
  # amqp gem cannot solve every possible race condition so be careful
end

If you do not need to assign the returned value to a variable, then the "block version" is recommended because it eliminates issues that may arise from attempts to use a connection object that is not fully opened yet. For example, handling of authentication failures is simpler with the block version, as we will see in the following sections. h3. Using AMQP.start EventMachine.run and {AMQP.connect} with a block is such a common combination that the amqp gem provides a shortcut:

require "amqp"

AMQP.start("amqp://dev.rabbitmq.com:5672") do |client|
  # connection is open and ready to be used
end

As these examples demonstrate, {AMQP.connect} and {AMQP.start} accept either a Hash of connection options or a connection URI string. See the reference documentation for each method to learn all of the options that they accept and what the default values are. h3. On Thread#sleep use When not passing a block to {AMQP.connect}, it is tempting to "give the connection some time to become established" by using Thread#sleep. Unless you are running the event loop in a separate thread, please do not do this. Thread#sleep blocks the current thread so that if the event loop is running in the current thread, blocking the thread _will also block the event loop_. *When the event loop is blocked, no data is sent or received, so the connection does not proceed.* h3. Detecting TCP connection failures When applications connect to the broker, they need to handle connection failures. Networks are not 100% reliable and even with modern system configuration tools, like "Chef":http://http://www.opscode.com/chef or "Puppet":http://http://www.puppetlabs.com, misconfigurations can happen. Also, the broker might be down for some reason. Ideally, error detection should happen as early as possible. There are two ways of detecting TCP connection failure, the first one is to catch an exception:

#!/usr/bin/env ruby
# encoding: utf-8

require "rubygems"
require "amqp"


puts "=> TCP connection failure handling with a rescue statement"
puts

connection_settings = {
  :port     => 9689,
  :vhost    => "/amq_client_testbed",
  :user     => "amq_client_gem",
  :password => "amq_client_gem_password",
  :timeout        => 0.3
}

begin
  AMQP.start(connection_settings) do |connection, open_ok|
    raise "This should not be reachable"
  end
rescue AMQP::TCPConnectionFailed => e
  puts "Caught AMQP::TCPConnectionFailed => TCP connection failed, as expected."
end

{AMQP.connect} (and {AMQP.start}) will raise {AMQP::TCPConnectionFailed} if the connection fails. Code that catches the error can write to a log about the issue or use retry to execute the begin block one more time. Because initial connection failures are due to misconfiguration or network outage, reconnection to the same endpoint (hostname, port, vhost combination) will result in the same error over and over again. TBD: failover, connection to the cluster. An alternative way of handling connection failure is with an _errback_ (a callback for a specific kind of error):

#!/usr/bin/env ruby
# encoding: utf-8

require "rubygems"
require "amqp"

puts "=> TCP connection failure handling with a callback"
puts

handler             = Proc.new { |settings| puts "Failed to connect, as expected"; EM.stop }
connection_settings = {
  :port     => 9689,
  :vhost    => "/amq_client_testbed",
  :user     => "amq_client_gem",
  :password => "amq_client_gem_password",
  :timeout        => 0.3,
  :on_tcp_connection_failure => handler
}


AMQP.start(connection_settings) do |connection, open_ok|
  raise "This should not be reachable"
end

the ":on_tcp_connection_failure" option accepts any object that responds to #call. If you connect to the broker from code in a class (as opposed to top-level scope in a script), Object#method can be used to pass an object method as a handler instead of a Proc. TBD: provide an example h3. Detecting authentication failures A connection may also fail due to authentication failure. Handling authentication failure is very similar to handling an initial TCP connection failure:

#!/usr/bin/env ruby
# encoding: utf-8

require "rubygems"
require "amqp"

puts "=> Authentication failure handling with a callback"
puts

handler             = Proc.new { |settings| puts "Failed to connect, as expected"; EM.stop }
connection_settings = {
  :port     => 5672,
  :vhost    => "/amq_client_testbed",
  :user     => "amq_client_gem",
  :password => "amq_client_gem_password_that_is_incorrect #{Time.now.to_i}",
  :timeout        => 0.3,
  :on_tcp_connection_failure => handler,
  :on_possible_authentication_failure => Proc.new { |settings|
                                            puts "Authentication failed, as expected, settings are: #{settings.inspect}"

                                            EM.stop
                                          }
}

AMQP.start(connection_settings) do |connection, open_ok|
  raise "This should not be reachable"
end

default handler raises {AMQP::PossibleAuthenticationFailureError}:

#!/usr/bin/env ruby
# encoding: utf-8

require "rubygems"
require "amqp"

puts "=> Authentication failure handling with a rescue block"
puts

handler             = Proc.new { |settings| puts "Failed to connect, as expected"; EM.stop }
connection_settings = {
  :port     => 5672,
  :vhost    => "/amq_client_testbed",
  :user     => "amq_client_gem",
  :password => "amq_client_gem_password_that_is_incorrect #{Time.now.to_i}",
  :timeout        => 0.3,
  :on_tcp_connection_failure => handler
}


begin
  AMQP.start(connection_settings) do |connection, open_ok|
    raise "This should not be reachable"
  end
rescue AMQP::PossibleAuthenticationFailureError => afe
  puts "Authentication failed, as expected, caught #{afe.inspect}"
  EventMachine.stop if EventMachine.reactor_running?
end

In case you are wondering why the callback name has "possible" in it, {http://bit.ly/amqp091spec AMQP 0.9.1 spec} requires broker implementations to simply close the TCP connection without sending any more data when an exception, such as authentication failure, occurs before the AMQP connection is open. In practice, however, when a broker closes a TCP connection after a successful TCP connection has been established but before an AMQP connection is open, it means that authentication has failed. h2. Starting the event loop and connecting in Web applications (Ruby on Rails, Sinatra, Merb, Rack) Web applications are different from standalone applications in that the main thread is occupied by a Web/application server like Unicorn or Thin, so you need to start the EventMachine reactor before you attempt to use {AMQP.connect}. In a Ruby on Rails application, probably the best place for this is in the initializer (like config/initializers/amqp.rb). For Merb applications it is config/init.rb. For Sinatra and pure Rack applications, place it next to the other configuration code. Next, we are going to discuss issues specific to particular Web servers. h3. Using Ruby amqp gem with Unicorn h4. Unicorn is a pre-forking server "Unicorn":http://unicorn.bogomips.org is a pre-forking server. That means it forks worker processes that serve HTTP requests. The "fork(2)":http://en.wikipedia.org/wiki/Fork_(operating_system) system call has several gotchas associated with it, two of which affect EventMachine and the "Ruby amqp gem":http://github.com/ruby-amqp/amqp: * Unintentional file descriptor sharing * The fact that a "forked child process only inherits one thread":http://bit.ly/fork-and-threads and therefore the EventMachine thread is not inherited To avoid both problems, start the EventMachine reactor and AMQP connection *after* the master process forks workers. The master Unicorn process never serves HTTP requests and usually does not need to hold an AMQP connection. Next, let us see how to spin up the EventMachine reactor and connect to the broker after Unicorn forks a worker. h4. Starting the EventMachine reactor and connecting to the broker after Unicorn forks worker processes Unicorn lets you specify a configuration file to use. In that file you define a callback that Unicorn runs after it forks worker process(es):

ENV["FORKING"] = "true"

listen 3000

worker_processes 1
timeout          30

preload_app true


after_fork do |server, worker|
  require "amqp"

  # the following is *required* for Rails + "preload_app true",
  defined?(ActiveRecord::Base) and
    ActiveRecord::Base.establish_connection


  t = Thread.new { AMQP.start }
  sleep(1.0)

  EventMachine.next_tick do
    AMQP.channel ||= AMQP::Channel.new(AMQP.connection)
    AMQP.channel.queue("amqpgem.examples.rails23.warmup", :durable => true)

    3.times do |i|
      puts "[after_fork/amqp] Publishing a warmup message ##{i}"

      AMQP.channel.default_exchange.publish("A warmup message #{i} from #{Time.now.strftime('%H:%M:%S %m/%b/%Y')}", :routing_key => "amqpgem.examples.rails23.warmup")
    end
  end
end

In the example above we start the EventMachine reactor in a separate thread, block the current thread for 1 second to let the event loop spin up and then connect to the AMQP broker on the next event loop tick. Publishing several warmup messages on boot is a good idea because it allows the early detection of issues that forking may cause. Note that a configuration file can easily be used in development environments because, other than the fact that Unicorn runs in the foreground, it gives you exactly the same application boot behavior as in QA and production environments. An "example Ruby on Rails application that uses the Ruby amqp gem and Unicorn":http://bit.ly/ruby-amqp-gem-example-with-ruby-on-rails-and-unicorn is available on GitHub. h3. Using the Ruby amqp gem with Passenger "Phusion Passenger":http://www.modrails.com is also a pre-forking server, and just as with Unicorn, the EventMachine reactor and AMQP connection should be started *after* it forks worker processes. The Passenger documentation has "a section":http://bit.ly/passenger-forking-gotchas that explains how to avoid problems related to the behavior of the fork(2) system call, namely: * Unintentional file descriptor sharing * The fact that a "forked child process only inherits one thread":http://bit.ly/fork-and-threads and therefore the EventMachine thread is not inherited h4. Using an event handler to spawn one amqp connection per worker Passenger provides a hook that you should use for spawning AMQP connections:

if defined?(PhusionPassenger) # otherwise it breaks rake commands if you put this in an initializer
  PhusionPassenger.on_event(:starting_worker_process) do |forked|
    if forked
      # We're in a smart spawning mode
      # Now is a good place to connect to the broker
    end
  end
end

Basically, the recommended default smart spawn mode works exactly the same as in Unicorn (with all of the same common pitfalls). An "example application":http://bit.ly/ruby-amqp-gem-example-with-ruby-on-rails-and-passenger is available on github. h3. Using the Ruby amqp gem with Thin and Goliath h4. Thin and Goliath start the EventMachine reactor for you, but there is a little nuance If you use "Thin":http://code.macournoyer.com/thin/ or "Goliath":https://github.com/postrank-labs/goliath/, you are all set because those two servers use EventMachine under the hood. There is no need to start the EventMachine reactor. However, depending on the application server, its version, the version of the framework and Rack middleware being used, EventMachine reactor start may be slightly delayed. To overcome this potential difficulty, use EventMachine.next_tick to delay connection until after the reactor is actually running:

EventMachine.next_tick { AMQP.connect(...) }

So, in case the EventMachine reactor is not yet running on server/application boot, the connection will not fail but will instead wait for the reactor to start. Thin and Goliath are not pre-forking servers so there is no need to re-establish the connection as you do with Unicorn and Passenger. h2. If it just does not work: troubleshooting If you have read this guide and your issue is still unresolved, check our {file:docs/Troubleshooting.textile Troubleshooting guide} before asking on the mailing list. h2. What to read next * {file:docs/Queues.textile Working With Queues}. This guide focuses on features consumer applications use heavily. * {file:docs/Exchanges.textile Working With Exchanges}. This guide focuses on features producer applications use heavily. * {file:docs/ErrorHandling.textile Error handling} * {file:docs/ConnectionEncryptionWithTLS.textile Using TLS (SSL)} (if you want to use an SSL encrypted connection to the broker) h2. Authors This guide was written by "Michael Klishin":http://twitter.com/michaelklishin and edited by "Chris Duncan":https://twitter.com/celldee. h2. Tell us what you think! Please take a moment to tell us what you think about this guide "on Twitter":http://twitter.com/rubyamqp or the "Ruby AMQP mailing list":http://groups.google.com/group/ruby-amqp. Let us know what was unclear or what has not been covered. Maybe you do not like the guide style or grammar or discover spelling mistakes. Reader feedback is key to making the documentation better. If, for some reason, you cannot use the communication channels mentioned above, you can "contact the author of the guides directly":mailto:michael@novemberain.com?subject=amqp%20gem%20documentation
amqp-1.8.0/docs/ErrorHandling.textile0000644000004100000410000006052513321132064017577 0ustar www-datawww-data# @title Ruby AMQP gem: Error handling and recovery h1. Error handling and recovery h2. This Documentation Has Moved to rubyamqp.info amqp gem documentation guides are now hosted on "rubyamqp.info":http://rubyamqp.info. h2. About this guide Development of a robust application, be it message publisher or message consumer, involves dealing with multiple kinds of failures: protocol exceptions, network failures, broker failures and so on. Correct error handling and recovery is not easy. This guide explains how amqp gem helps you in dealing with issues like * Initial broker connection failures * Network connection interruption * AMQP connection-level exceptions * AMQP channel-level exceptions * Broker failure * TLS (SSL) related issues as well as * How to recover after a network failure * What is automatic recovery mode, when you should and should not use it This work is licensed under a Creative Commons Attribution 3.0 Unported License (including images & stylesheets). The source is available "on Github":https://github.com/ruby-amqp/amqp/tree/master/docs. h2. Covered versions This guide covers "Ruby amqp gem":http://github.com/ruby-amqp/amqp v0.8.0.RC14 and later. h2. Code examples There are several {https://github.com/ruby-amqp/amqp/tree/master/examples/error_handling examples} in the git repository dedicated to the topic of error handling and recovery. Feel free to contribute new examples. h3. Initial broker connection failures When applications connect to the broker, they need to handle connection failures. Networks are not 100% reliable, even with modern system configuration tools like Chef or Puppet misconfigurations happen and broker might be down, too. Error detection should happen as early as possible. There are two ways of detecting TCP connection failure, the first one is to catch an exception:

begin
  AMQP.start(connection_settings) do |connection, open_ok|
    raise "This should not be reachable"
  end
rescue AMQP::TCPConnectionFailed => e
  puts "Caught AMQP::TCPConnectionFailed => TCP connection failed, as expected."
end

Full example: {AMQP.connect} (and {AMQP.start}) will raise {AMQP::TCPConnectionFailed} if connection fails. Code that catches it can write to log about the issue or use retry to execute begin block one more time. Because initial connection failures are due to misconfiguration or network outage, reconnection to the same endpoint (hostname, port, vhost combination) will result in the same issue over and over. TBD: failover, connection to the cluster. Alternative way of handling connection failure is with an errback (a callback for specific kind of error):

handler             = Proc.new { |settings| puts "Failed to connect, as expected"; EventMachine.stop }
connection_settings = {
  :port     => 9689,
  :vhost    => "/amq_client_testbed",
  :user     => "amq_client_gem",
  :password => "amq_client_gem_password",
  :timeout        => 0.3,
  :on_tcp_connection_failure => handler
}

Full example: :on_tcp_connection_failure option accepts any object that responds to #call. If you connect to the broker from a code in a class (as opposed to top-level scope in a script), Object#method can be used to pass object method as a handler instead of a Proc. TBD: provide an example h3. Authentication failures Another reason why connection may fail is authentication failure. Handling authentication failure is very similar to handling initial TCP connection failure: h4. Default handler default handler raises {AMQP::PossibleAuthenticationFailureError}: In case you wonder why callback name has "possible" in it: {http://bit.ly/amqp091spec AMQP 0.9.1 spec} requires broker implementations to simply close TCP connection without sending any more data when an exception (such as authentication failure) occurs before AMQP connection is open. In practice, however, when broker closes TCP connection between successful TCP connection and before AMQP connection is open, it means that authentication has failed. h2. Handling network connection interruptions Network connectivity issues are sad fact of life in modern software systems. Event small products and projects these days consist of multiple applications, often running on more than one machine. Ruby amqp gem detects TCP connection failures and lets you handle them by defining a callback using {AMQP::Session#on_tcp_connection_loss}. That callback will be run when TCP connection fails, and will be passed two parameters: connection object and settings of the last successful connection.

connection.on_tcp_connection_loss do |connection, settings|
  # reconnect in 10 seconds, without enforcement
  connection.reconnect(false, 10)
end

Sometimes it is necessary for other entities in an application to react to network failures. amqp gem 0.8.0 and later provides a number event handlers to make this task easier for developers. This set of features is know as the "shutdown protocol" (the word "protocol" here means "API interface" or "behavior", not network protocol). {AMQP::Session}, {AMQP::Channel}, {AMQP::Exchange}, {AMQP::Queue} and {AMQP::Consumer all implement shutdown protocol and thus error handling API is consistent for all classes, with {AMQP::Session} and #{AMQP::Channel} having a few methods other entities do not have. The Shutdown protocol revolves around two events: * Network connection fails * Broker closes AMQP connection (or channel) In this section, we concentrate only on the former. When network connection fails, the underlying networking library detects it and runs a piece of code on {AMQP::Session} to handle it. That, in turn, propagates this event to channels, channels propagate it to exchanges and queues, queues propagate it to their consumers (if any). Each of these entities in the object graph can react to network interruption by executing application-defined callbacks. h3. Shutdown Protocol methods on AMQP::Session * {AMQP::Session#on_tcp_connection_loss} * {AMQP::Session#on_connection_interruption} The difference between these methods is that {AMQP::Session#on_tcp_connection_loss} is used to define a callback that will be executed *once* when TCP connection fails. It is possible that reconnection attempts will not succeed immediately, so there will be subsequent failures. To react to those, {AMQP::Session#on_connection_interruption} method is used. First argument that both of these methods yield to the handler your application defines is the connection itself. This is done to make sure you can register Ruby objects as handlers, and they do not have to keep any state around (for example, connection instances):

connection.on_connection_interruption do |conn|
  puts "Connection detected connection interruption"
end

# or

class ConnectionInterruptionHandler

  #
  # API
  #

  def handle(connection)
    # handling logic
  end

end

handler = ConnectionInterruptionHandler.new
connection.on_connection_interruption(&handler.method(:handle))

Note that {AMQP::Session#on_connection_interruption} callback is called *before* this event is propagated to channels, queues and so on. Different applications handle connection failures differently. It is very common to use {AMQP::Session#reconnect} method to schedule a reconnection to the same host, or use {AMQP::Session#reconnect_to} to connect to a different one. For some applications it is OK to simply exit and wait to be restarted at a later point in time, for example, by a process monitoring system like Nagios or Monit. h3. Shutdown Protocol methods on AMQP::Channel {AMQP::Channel} provides only one method: {AMQP::Channel#on_connection_interruption}, that registers a callback similar to the one seen in the previous section:

channel.on_connection_interruption do |ch|
  puts "Channel #{ch.id} detected connection interruption"
end

Note that {AMQP::Channel#on_connection_interruption} callback is called *after* this event is propagated to exchanges, queues and so on. Right after that channel state is reset, except for error handling/recovery-related callbacks. Many applications do not need per-channel network failure handling. h3. Shutdown Protocol methods on AMQP::Exchange {AMQP::Exchange} provides only one method: {AMQP::Exchange#on_connection_interruption}, that registers a callback similar to the one seen in the previous section:

exchange.on_connection_interruption do |ex|
  puts "Exchange #{ex.name} detected connection interruption"
end

Many applications do not need per-exchange network failure handling. h3. Shutdown Protocol methods on AMQP::Queue {AMQP::Queue} provides only one method: {AMQP::Queue#on_connection_interruption}, that registers a callback similar to the one seen in the previous section:

queue.on_connection_interruption do |q|
  puts "Queue #{q.name} detected connection interruption"
end

Note that {AMQP::Queue#on_connection_interruption} callback is called *after* this event is propagated to consumers. Many applications do not need per-queue network failure handling. h3. Shutdown Protocol methods on AMQP::Consumer {AMQP::Consumer} provides only one method: {AMQP::Consumer#on_connection_interruption}, that registers a callback similar to the one seen in the previous section:

consumer.on_connection_interruption do |c|
  puts "Consumer with consumer tag #{c.consumer_tag} detected connection interruption"
end

Many applications do not need per-consumer network failure handling. h2. Recovering from network connection failures Detecting network connections is nearly useless if AMQP-based application cannot recover from them. Recovery is the hard part in "error handling and recovery". Fortunately, recovery process for many applications follows one simple scheme that amqp gem can perform automatically for you. Recovery process, both manual and automatic, always begins with re-opening AMQP connection and then all the channels on that connection. h3. Manual recovery Similarly to the Shutdown Protocol, amqp gem entities implement Recovery Protocol. Recovery Protocol consists of 3 methods connections, channels, queues, consumers and exchanges implement: * {AMQP::Session#before_recovery} * {AMQP::Session#auto_recover} * {AMQP::Session#after_recovery} {AMQP::Session#before_recovery} lets application developers register a callback that will be executed *after TCP connection is re-established but before AMQP connection is reopened*. {AMQP::Session#after_recovery} is similar except that the callback is run *after AMQP connection is reopened*. {AMQP::Channel}, {AMQP::Queue}, {AMQP::Consumer} and {AMQP::Exchange} methods behavior is identical. Recovery process for AMQP applications usually involves the following steps: # Re-open AMQP connection. # Once connection is open again, re-open all AMQP channels on that connection. # For each channel, re-declare all exchanges # For each channel, re-declare all queues # Once queue is declared, for each queue, re-register all bindings # Once queue is declared, for each queue, re-register all consumers h3. Automatic recovery Many applications use the same recovery strategy, that consists of the following steps: * Re-open channels * For each channel, re-declare exchanges * For each channel, re-declare queues * For each queue, recover all bindings * For each queue, recover all consumers amqp gem provides a feature known as "automatic recovery" that is *opt-in* (not opt-out, not used by default) and lets application developers get aforementioned recovery strategy by setting one additional attribute on {AMQP::Channel} instance:

ch = AMQP::Channel.new(connection)
ch.auto_recovery = true

A more verbose way to do the same thing:

ch = AMQP::Channel.new(connection, AMQP::Channel.next_channel_id, :auto_recovery => true)

Note that if you do not want to pass any options, 2nd argument can be left out as well, then it will default to {AMQP::Channel.next_channel_id}. To find out whether channel uses automatic recovery mode, use {AMQP::Channel#auto_recovering?}. Auto recovery mode can be turned on and off any number of times during channel life cycle, although very small percentage of applications really does this. Typically you decide what channels should be using automatic recovery at application design stage. Full example (run it, then shut down AMQP broker running on localhost, then bring it back up and use management tools such as `rabbitmqctl` to see that queues & bindings & consumer have all recovered): Server-named queues, when recovered automatically, will get *new server-generated names* to guarantee there are no name collisions. When in doubt, try using automatic recovery first. If it is not sufficient for you application, switch to manual recovery using events and callbacks introduced in the "Manual recovery" section. h2. Detecting broker failures AMQP applications see broker failure as TCP connection loss. There is no reliable way to know whether there is a network split or network peer is down. h2. AMQP connection-level exceptions h3. Handling connection-level exceptions Connection-level exceptions are rare and may indicate a serious issue with client library or in-flight data corruption. They mandate that connection cannot be used any more and must be closed. In any case, your application should be prepared to handle this kind of errors. To define a handler, use {AMQP::Session#on_error} method that takes a callback and yields two arguments to it when connection-level exception happens:

connection.on_error do |conn, connection_close|
  puts "Handling a connection-level exception."
  puts
  puts "AMQP class id : #{connection_close.class_id}"
  puts "AMQP method id: #{connection_close.method_id}"
  puts "Status code   : #{connection_close.reply_code}"
  puts "Error message : #{connection_close.reply_text}"
end

Status codes are similar to those of HTTP. For the full list of error codes and their meaning, consult {http://www.rabbitmq.com/amqp-0-9-1-reference.html#constants AMQP 0.9.1 constants reference}. Only one connection-level exception handler can be defined per connection instance (the one added last replaces previously added ones). Full example: h2. Handling graceful broker shutdown When AMQP broker is shut down, it properly closes connection first. To do so, it uses *connection.close* AMQP method. AMQP clients then need to check if the reply code is equal to 320 (CONNECTION_FORCED) to distinguish graceful shutdown. With RabbitMQ, when broker is stopped using
rabbitmqctl stop
reply_text will be set to
CONNECTION_FORCED - broker forced connection closure with reason 'shutdown'
Each application choose how to handle graceful broker shutdowns individually, so *amqp gem's automatic reconnection does not cover graceful broker shutdowns*. Applications that want to reconnect when broker is stopped can use {AMQP::Session#periodically_reconnect} like so:

connection.on_error do |conn, connection_close|
  puts "[connection.close] Reply code = #{connection_close.reply_code}, reply text = #{connection_close.reply_text}"
  if connection_close.reply_code == 320
    puts "[connection.close] Setting up a periodic reconnection timer..."
    # every 30 seconds
    conn.periodically_reconnect(30)
  end
end

Once AMQP connection is re-opened, channels in automatic recovery mode will recover just like they do after network outages. h2. Integrating channel-level exceptions handling with object-oriented Ruby code Error handling can be easily integrated into object-oriented Ruby code (in fact, this is highly encouraged). A common technique is to combine {http://rubydoc.info/stdlib/core/1.8.7/Object:method Object#method} and {http://rubydoc.info/stdlib/core/1.8.7/Method:to_proc Method#to_proc} and use object methods as error handlers:

class ConnectionManager

  #
  # API
  #

  def connect(*args, &block)
    @connection = AMQP.connect(*args, &block)

    # combines Object#method and Method#to_proc to use object
    # method as a callback
    @connection.on_error(&method(:on_error))
  end # connect(*args, &block)


  def on_error(connection, connection_close)
    puts "Handling a connection-level exception."
    puts
    puts "AMQP class id : #{connection_close.class_id}"
    puts "AMQP method id: #{connection_close.method_id}"
    puts "Status code   : #{connection_close.reply_code}"
    puts "Error message : #{connection_close.reply_text}"
  end # on_error(connection, connection_close)
end

Full example that uses objects: TBD h2. AMQP channel-level exceptions h3. Handling channel-level exceptions Channel-level exceptions are more common than connection-level ones. They are handled in a similar manner, by defining a callback with {AMQP::Channel#on_error} method that takes a callback and yields two arguments to it when channel-level exception happens:

channel.on_error do |ch, channel_close|
  puts "Handling a channel-level exception."
  puts
  puts "AMQP class id : #{channel_close.class_id}"
  puts "AMQP method id: #{channel_close.method_id}"
  puts "Status code   : #{channel_close.reply_code}"
  puts "Error message : #{channel_close.reply_text}"
end

Status codes are similar to those of HTTP. For the full list of error codes and their meaning, consult {http://www.rabbitmq.com/amqp-0-9-1-reference.html#constants AMQP 0.9.1 constants reference}. Only one channel-level exception handler can be defined per channel instance (the one added last replaces previously added ones). Full example: h3. Integrating channel-level exceptions handling with object-oriented Ruby code Error handling can be easily integrated into object-oriented Ruby code (in fact, this is highly encouraged). A common technique is to combine {http://rubydoc.info/stdlib/core/1.8.7/Object:method Object#method} and {http://rubydoc.info/stdlib/core/1.8.7/Method:to_proc Method#to_proc} and use object methods as error handlers. For example of this, see section on connection-level exceptions above. Because channel-level exceptions may be raised because of multiple unrelated reasons and often indicate misconfigurations, how they are handled is very specific to particular applications. A common strategy is to log an error and then open and use another channel. h3. Common channel-level exceptions and what they mean A few channel-level exceptions are common and deserve more attention. h4. 406 Precondition Failed
Description
The client requested a method that was not allowed because some precondition failed.
What might cause it
Example RabbitMQ error message
h4. 405 Resource Locked
Description
The client attempted to work with a server entity to which it has no access because another client is working with it.
What might cause it
Example RabbitMQ error message
RESOURCE_LOCKED - cannot obtain exclusive access to locked queue 'amqpgem.examples.queue' in vhost '/'
h4. 403 Access Refused
Description
The client attempted to work with a server entity to which it has no access due to security settings.
What might cause it
Application tries to access a queue or exchange it has no permissions for (or right kind of permissions, for example, write permissions)
Example RabbitMQ error message
ACCESS_REFUSED - access to queue 'amqpgem.examples.channel_exception' in vhost 'amqp_gem_testbed' refused for user 'amqp_gem_reader'
h2. TLS (SSL) related issues TBD h2. Conclusion Distributed applications introduce a whole new class of failures developers need to be aware of. Many of them come from unreliability of the network. The famous "Fallacies of Distributed Computing":http://en.wikipedia.org/wiki/Fallacies_of_Distributed_Computing list common assumptions software engineers must not make: * The network is reliable. * Latency is zero. * Bandwidth is infinite. * The network is secure. * Topology doesn't change. * There is one administrator. * Transport cost is zero. * The network is homogeneous. Unfortunately, applications that use Ruby and AMQP are not immune to these problems and developers need to always keep that in mind. This list is just as relevant in 2011 as it was in the 90s. Ruby amqp gem 0.8.x and later lets applications to define handlers that handle connection-level exceptions, channel-level exceptions and TCP connection failures. Handling AMQP exceptions and network connection failures is relatively easy. Re-declaring AMQP instances application works with is where the most of complexity comes from. By using Ruby objects as error handlers, declaration of AMQP entities can be done in one place, making it much easier to understand and maintain. amqp gem error handling and interruption is not a copy of RabbitMQ Java client's "Shutdown Protocol":http://www.rabbitmq.com/api-guide.html#shutdown, but they turn out to be similar with respect to network failures and connection-level exceptions. TBD h2. Authors This guide was written by "Michael Klishin":http://twitter.com/michaelklishin and edited by "Chris Duncan":https://twitter.com/celldee. h2. Tell us what you think! Please take a moment and tell us what you think about this guide "on Twitter":http://twitter.com/rubyamqp or "Ruby AMQP mailing list":http://groups.google.com/group/ruby-amqp: What was unclear? What wasn't covered? Maybe you don't like guide style or grammar and spelling are incorrect? Readers feedback is key to making documentation better. If mailing list communication is not an option for you for some reason, you can "contact guides author directly":mailto:michael@novemberain.com?subject=amqp%20gem%20documentation
amqp-1.8.0/docs/TestingWithEventedSpec.textile0000644000004100000410000002013413321132064021430 0ustar www-datawww-data# @title Ruby AMQP gem: Testing AMQP applications h1. Testing AMQP applications h2. This Documentation Has Moved to rubyamqp.info amqp gem documentation guides are now hosted on "rubyamqp.info":http://rubyamqp.info. h2. About this guide This guide covers unit testing of amqp-based applications, primarily using "evented-spec":http://github.com/ruby-amqp/evented-spec. h2. Covered versions This guide covers "Ruby amqp gem":http://github.com/ruby-amqp/amqp v0.8.0 and later as well as "evented-spec gem":http://github.com/ruby-amqp/evented-spec v0.9.0 and later. h2. Rationale The AMQP protocol is inherently asynchronous. Testing of asynchronous code is often more difficult than synchronous code. There are two approaches to it: * Stubbing out a big chunk of the environment * Using the "real" environment The former is risky because your application becomes divorced from actual behavior of other applications. The latter approach is more reliable but at the same time more tedious, because there is certain amount of incidental complexity that "real" environment carries. However, a lot of this complexity can be eliminated with tools and libraries. The evented-spec gem is one of those tools. It grew out of necessity to test "amqp Ruby gem":http://github.com/ruby-amqp/amqp and has provent itself to be both very powerful and easy to use. This guide covers usage of that gem in context of applications that use amqp gem but can also be useful for testing EventMachine and Cool.io-based applications. h2. Using evented-spec h3. Setting up To start using amqp all you need is to include EventedSpec::AMQPSpec module into your context and add #done calls to your examples: h3. Testing in the Asynchronous Environment Since we are using callback mechanisms in order to provide asynchronicity, we have to deal with situation when we expect a response, and response never comes. Usual solution includes setting a timeout which makes the given tests fail if they aren't finished in a timely manner. When #done is called, your tests confirm successful ending of specs. Try removing done from the above example and see what happens. (spoiler: EventedSpec::SpecHelper::SpecTimeoutExceededError: Example timed out) h3. The #done method The *#done* method call is a hint for evented-spec to consider the example finished. If this method is not called, example will be forcefully terminated after a certain period of time or "time out". This means there are two approaches to testing of asynchronous code: * Have timeout value high enough for all operations to finish (for example, expected number of messages is received). * Call #done when some condition holds true (for example, message with a specific property or payload is received). The latter approach is recommended because it makes tests less dependent on machine-specific throughput or timing: it is very common for continuous integration environments to use virtual machines that are significantly less powerful than machines developers use, so timeouts have to be carefully adjusted to work in both settings. h3. Default Connection Options and Timeout It is sometimes desirable to use custom connection settings for your test environment as well as the default timeout value used. evented-spec lets you do it: Available options are passed to {AMQP.connect} so it is possible to specify host, port, vhost, username and password your test suite needs. h3. Lifecycle Callbacks evented-spec provides various callbacks similar to rspec's before(:each) / after(:each). They are called amqp_before and amqp_after and happen right after connection is established or before connection is closed. It is a good place to put your channel initialization routines. h3. Full Example Now that you're filled on theory part, it's time to do something with all this knowledge. Below goes a slightly modified version of one of the integration specs from AMQP suite. It sets up default topic exchange and publishes various messages about sports events: Couple of things to notice: #done is invoked using an optional callback and optional delay, also instance variables behavior in hooks is the same as in "normal" rspec hooks. h3. Using #delayed AMQP gem uses "EventMachine":http://eventmachine.rubyforge.org/ under hood. If you don't know about eventmachine, you can read more about it on the official site. What's important for us is that you *cannot use sleep for delays*. Why? Because all the specs code is processed directly in the "reactor":http://en.wikipedia.org/wiki/Reactor_pattern thread, if you sleep in that thread, reactor cannot send frames. What you need to use instead is #delayed method. It takes delay time in seconds and callback which it launches once that time passes. Basic usage is either sleep replacement or ensuring certain order of execution (though, the latter should not bother you too much). You can also use it to cleanup your environment after tests if any is needed. In the following example, we declare two channels, then declare the same queue twice with the same name but different options (which raises a channel-level exception in AMQP): If you draw a timeline, various events happen at 0.0s, then at 0.1s, then at 0.3s and eventually at 0.4s. h3. Design For Testability As *Integration With Objects* section of the {file:docs/GettingStarted.textile Getting Started with Ruby amqp gem and RabbitMQ} demonstrates, good object-oriented design often makes it possible to test AMQP consumers in isolation without connecting to the broker or even starting EventMachine even loop. All the "Design for testability" practices apply fully to AMQP application testing. h3. Real worldExamples Please refer to the "amqp gem test suite":https://github.com/ruby-amqp/amqp/tree/master/spec to see evented-spec in action. h3. How evented-spec Works When you include EventedSpec::AMQPSpec module, #it calls are wrapped in EventMachine.start + AMQP.connect calls, so you can start writing your examples as if you're connected. Please note that you still need to open your own channel(s). h2. What to read next There is a lot more to evented-spec than described in this guide. "evented-spec documentation":http://rdoc.info/github/ruby-amqp/evented-spec/master covers that gem in more detail gem. For more code examples, see "amqp Ruby gem test suite":https://github.com/ruby-amqp/amqp/tree/master/spec. h2. Tell us what you think! Please take a moment and tell us what you think about this guide "on Twitter":http://twitter.com/rubyamqp or "Ruby AMQP mailing list":http://groups.google.com/group/ruby-amqp: what was unclear? what wasn't covered? maybe you don't like guide style or grammar and spelling are incorrect? Readers feedback is key to making documentation better. If mailing list communication is not an option for you for some reason, you can "contact guides author directly":mailto:michael@novemberain.com?subject=amqp%20gem%20documentation
amqp-1.8.0/docs/Bindings.textile0000644000004100000410000001527613321132064016601 0ustar www-datawww-data# @title Ruby AMQP gem: Bindings h1. Working With Bindings h2. This Documentation Has Moved to rubyamqp.info amqp gem documentation guides are now hosted on "rubyamqp.info":http://rubyamqp.info. h2. About this guide This guide covers bindings in AMQP 0.9.1, what they are, what role do they play and how to accomplish typical operations using Ruby amqp gem. This work is licensed under a Creative Commons Attribution 3.0 Unported License (including images & stylesheets). The source is available "on Github":https://github.com/ruby-amqp/amqp/tree/master/docs. h2. Covered versions This guide covers "Ruby amqp gem":http://github.com/ruby-amqp/amqp v0.8.0 and later. h2. Bindings in AMQP 0.9.1 Bindings are rules that exchanges use (among other things) to route messages to queues. To instruct an exchange E to route messages to a queue Q, Q has to _be bound_ to E. Bindings may have an optional _routing key_ attribute used by some exchange types. The purpose of the routing key is to selectively match only specific (matching) messages published to an exchange to the bound queue. In other words, the routing key acts like a filter. Learn more about how bindings fit into the AMQP Model in the {file:docs/AMQP091ModelExplained.textile AMQP 0.9.1 Model Explained} documentation guide. h2. What Are AMQP 0.9.1 Bindings Bindings are rules that exchanges use (among other things) to route messages to queues. To instruct an exchange E to route messages to a queue Q, Q has to _be bound_ to E. Bindings may have an optional _routing key_ attribute used by some exchange types. The purpose of the routing key is to selectively match only specific (matching) messages published to an exchange to the bound queue. In other words, the routing key acts like a filter. To draw an analogy: * Queue is like your destination in New York city * Exchange is like JFK airport * Bindings are routes from JFK to your destination. There can be none or more than one way to reach it Some exchange types use routing key while some others do not (and route messages unconditionally or based on message metadata). If AMQP message cannot be routed to any queue (for example, because there are no bindings for the exchange it was published to), it is either dropped or returned to the publisher, depending on message attributes the publisher has set. If application wants to connect a queue to an exchange, it needs to _bind_ them. The opposite operation is called _unbinding_. h2. Binding queues to exchanges In order to receive messages, a queue needs to be bound to at least one exchange. Most of the time binding is explcit (done by applications). To bind a queue to an exchange, use {AMQP::Queue#bind} where the argument passed can be either an {AMQP::Exchange} instance or a string.

queue.bind(exchange) do |bind_ok|
  puts "Just bound #{queue.name} to #{exchange.name}"
end

Full example: The same example using a string without callback:

queue.bind("amq.fanout")

Full example: h2. Unbinding queues from exchanges To unbind a queue from an exchange use {AMQP::Queue#unbind}:

queue.unbind(exchange)

Full example: Trying to unbind a queue from an exchange that the queue was never bound to will result in a channel-level exception. h2. Bindings, Routing and Returned Messages h3. How AMQP 0.9.1 Brokers Route Messages After AMQP message reaches AMQP broker and before it reaches a consumer, several things happen: * AMQP broker needs to find one or more queues the message needs to be routed to, depending on exchange * AMQP broker puts a copy of the message into each of those queues or decides to return the messages to the publisher * AMQP broker pushes messages to consumers on those queues or waits for applications to fetch them on demand A more in-depth description is this: * AMQP broker needs to consult bindings list for the exchange the message was published to find one or more queues the message needs to be routed to (step 1) * If there are no suitable queues found during step 1 and the message was published as mandatory, it is returned to the publisher (step 1b) * If there are suitable queues, a _copy_ of the message is placed into each one (step 2) * If the message was published as mandatory, but there are no active consumers for it, it is returned to the publisher (step 2b) * If there are active consumers on those queues and basic.qos setting permits, message is pushed to those consumers (step 3) * If there are no active consumers and the message is *not* published as mandatory, it will be left in the queue The takeaway is that messages may or may not be routed and it is important for applications to handle unroutable messages. h3. Handling of Unroutable Messages Unroutable messages are either dropped or returned back to producers. Vendor-specific extensions can provide additional ways of handing of unroutable messages: for example, RabbitMQ's "Alternate Exchanges extension":http://www.rabbitmq.com/extensions.html#alternate-exchange makes it possible to route unroutable messages to another exchange. amqp gem support for it is documented in the {file:docs/VendorSpecificExtensions.textile Vendor-specific Extensions guide}. RabbitMQ 2.6 will introduce a new feature callled dead letter queue where unroutable messages will be put instead of dropping them. amqp gem provides a way to handle returned messages with the {AMQP::Exchange#on_return} method:

exchange.on_return do |basic_return, metadata, payload|
  puts "#{payload} was returned! reply_code = #{basic_return.reply_code}, reply_text = #{basic_return.reply_text}"
end

{file:docs/Exchanges.textile Working With Exchanges} documentation guide provides more information on the subject, including full code examples. h2. Authors This guide was written by "Michael Klishin":http://twitter.com/michaelklishin and edited by "Chris Duncan":https://twitter.com/celldee. h2. Tell us what you think! Please take a moment and tell us what you think about this guide "on Twitter":http://twitter.com/rubyamqp or "Ruby AMQP mailing list":http://groups.google.com/group/ruby-amqp: what was unclear? what wasn't covered? maybe you don't like guide style or grammar and spelling are incorrect? Readers feedback is key to making documentation better. If mailing list communication is not an option for you for some reason, you can "contact guides author directly":mailto:michael@novemberain.com?subject=amqp%20gem%20documentation amqp-1.8.0/docs/Troubleshooting.textile0000644000004100000410000001603313321132064020223 0ustar www-datawww-data# @title Ruby AMQP gem: Troubleshooting and debugging AMQP applications h1. Troubleshooting and debugging AMQP applications h2. This Documentation Has Moved to rubyamqp.info amqp gem documentation guides are now hosted on "rubyamqp.info":http://rubyamqp.info. h2. About this guide This guide describes tools and strategies that help in troubleshooting and debugging applications that use AMQP in general and "Ruby AMQP gem":http://github.com/ruby-amqp/amqp in particular. h2. Covered versions This guide covers "Ruby amqp gem":http://github.com/ruby-amqp/amqp v0.8.0 and later. h2. First steps Whenever something doesn't work, check the following things before asking on the mailing list: * AMQP broker log. * List of users in a particular vhost you are trying to connect. * Network connectivity. We know, it's obvious, yet even experienced developers and devops engineers struggle with network access misconfigurations every once in a while. * If EventMachine is started in a separate thread, make sure that isn't dead. If it is, this usually means there was an exception that caused it to terminate, or environment it is running in "mixes threads and fork(2) system call":http://bit.ly/fork-and-threads. h2. Inspecting AMQP broker log file In this section we will cover typical problems that can be tracked down by reading AMQP broker log. We will use RabbitMQ as an example, however, different AMQP brokers often log most of the same issues. RabbitMQ logs abrupt TCP connection failures, timeouts, protocol version mismatches and so on. If you are running RabbitMQ, log locations for various operating systems and distributions is documented in the "RabbitMQ installation guide":http://www.rabbitmq.com/install.html On Mac OS X, RabbitMQ installed via Homebrew logs to $HOMEBREW_HOME/var/log/rabbitmq/rabbit@$HOSTNAME.log. For example, if you have Homebrew installed at /usr/local and your hostname is giove, log will be at /usr/local/var/log/rabbitmq/rabbit@giove.log. Here is what authentication failure looks like in RabbitMQ log:
=ERROR REPORT==== 17-May-2011::17:37:58 ===
exception on TCP connection <0.4770.0> from 127.0.0.1:46551
{channel0_error,starting,
                {amqp_error,access_refused,
                            "AMQPLAIN login refused: user 'pipeline_agent' - invalid credentials",
                            'connection.start_ok'}}
This means that connection attempt with username pipeline_agent failed because credentials were invalid. If you are seeing this message, make sure username, password *and vhost* are correct. The following entry:
=ERROR REPORT==== 17-May-2011::17:26:28 ===
exception on TCP connection <0.4201.62> from 10.8.0.30:57990
{bad_header,<<65,77,81,80,0,0,9,1>>}
Means that client supports AMQP 0.9.1 but broker doesn't (RabbitMQ versions pre-2.0 only support AMQP 0.8, for example). If you are using amqp gem 0.8 or later and seeing this entry in your broker log, you are connecting to AMQP broker that is too old to support this AMQP version. In case of RabbitMQ, make sure you run version 2.0 or later. TBD h2. Handling channel-level exceptions A broad range of problems result in AMQP channel exceptions: an indication by the broker that there was an issue application needs to be aware of. Channel-level exceptions are typically not fatal and can be recovered from. Some examples are: * Exchange is re-declared with attributes different from the original declaration. For example, a non-durable exchange is being re-declared as durable. * Queue is re-declared with attributes different from the original declaration. For example, an autodeletable queue is being re-declared as non-autodeletable. * Queue is bound to an exchange that does not exist. and so on. When troubleshooting AMQP applications, it is recommended that you detect and handle channel-level exceptions on all channels your application may use. For that, use {AMQP::Channel#on_error} method as demonstrated below:

events_channel.on_error do |ch, channel_close|
  puts "Channel-level exception on the events channel: #{channel_close.reply_text}"
end

commands_channel.on_error do |ch, channel_close|
  puts "Channel-level exception on the commands channel: #{channel_close.reply_text}"
end

Defining channel-level exception handlers will reveal many issues that it might take more time to detect using other troubleshooting techniques. h2. Testing network connection with AMQP broker using Telnet One simple way to check network connection between a particular network node and a node running AMQP broker is to use `telnet`:
telnet [host or ip] 5672
then enter any random string of text and hit Enter. AMQP broker should immediately close down the connection. Here is an example session:
telnet localhost 5672
Connected to localhost.
Escape character is '^]'.
adjasd
AMQP    Connection closed by foreign host.
If instead Telnet exits after printing
telnet: connect to address [host or ip]: Connection refused
telnet: Unable to connect to remote host
then connection from the machine you are running Telnet tests on and AMQP broker fails. This can be due to many different reasons, but it is a good idea to check these two things first: * Firewall configuration for port 5672 * DNS setup (if hostname is used) h2. Broker Startup Issues h3. Missing erlang-os-mon on Debian and Ubuntu The following error on RabbitMQ startup on Debian or Ubuntu
ERROR: failed to load application os_mon: {"no such file or directory","os_mon.app"}
suggests that *erlang-os-mon* package is not installed. h2. Authors This guide was written by "Michael Klishin":http://twitter.com/michaelklishin and edited by "Chris Duncan":https://twitter.com/celldee. h2. Tell us what you think! Please take a moment and tell us what you think about this guide "on Twitter":http://twitter.com/rubyamqp or "Ruby AMQP mailing list":http://groups.google.com/group/ruby-amqp: what was unclear? what wasn't covered? maybe you don't like guide style or grammar and spelling are incorrect? Readers feedback is key to making documentation better. If mailing list communication is not an option for you for some reason, you can "contact guides author directly":mailto:michael@novemberain.com?subject=amqp%20gem%20documentation
amqp-1.8.0/docs/08Migration.textile0000644000004100000410000002632413321132064017141 0ustar www-datawww-data# @title Ruby AMQP gem: Migrating to version 0.8.0 and later h1. amqp gem 0.8 migration guide h2. This Documentation Has Moved to rubyamqp.info amqp gem documentation guides are now hosted on "rubyamqp.info":http://rubyamqp.info. h2. About this guide This guide explains how (and why) applications that use amqp gem versions 0.6.x and 0.7.x should migrate to 0.8.0.RCs and future versions. It also outlines deprecated features, when and why they will be removed. This work is licensed under a Creative Commons Attribution 3.0 Unported License (including images & stylesheets). The source is available "on Github":https://github.com/ruby-amqp/amqp/tree/master/docs. h2. Covered versions This guide covers "Ruby amqp gem":http://github.com/ruby-amqp/amqp v0.6.x and v0.7.x. h2. Brief history of amqp gem amqp gem was born in early July 2008. Since then, it has seen 7 releases, used by all kinds of companies, have been key component to systems that process more than 100 gigabytes of data per day and, despite not being a "1.0 library", proven to be very usable. For the most part of 2008 and 2009 the gem did not receive much of attention. In 2010, some other AMQP clients (for example, RabbitMQ Java & .NET clients) switched to AMQP 0.9.1 specification. By the fall of 2010 it became clear that amqp gem needs a new team of maintainers, deep refactoring, new solid test suite and extensive documentation guides. Long story short, by December 2010, the "new maintainers group":https://github.com/ruby-amqp was formed and now maintains number of other libraries in the Ruby AMQP ecosystem, most notably the "Bunny gem":github.com/ruby-amqp/bunny. Over first few months of 2011, amqp gem undergone a number of maintenance releases (known as 0.7.x series), a ground up rewrite of the test suite, major documentation update as well as a major refactoring. It was upgraded to AMQP 0.9.1 specification and based on a new, much more efficient AMQP serialization library, "amq-protocol":https://github.com/ruby-amqp/amq-protocol. As part of this transition, amqp gem maintainers team pursued a number of goals: * Implement AMQP 0.9.1 spec plus all the RabbitMQ extensions * End "300 of 1 patch forks" situation that had gotten especially bad in early 2010 * Produce a solid end-to-end test suite that imitates real asynchronous applications * Resolve many API usability issues: inconsistent behavior, weird class and method naming, heavy reliance on implicit connection objects and so on * Produce documentation guides that other AMQP client users will be envious of * Define a stable public API * Improve memory efficiency and performance * Resolve many limitations of the original API and implementation design * Drop all the "experimental-stuff-that-was-never-finished" In the end, several projects like "evented-spec":https://github.com/ruby-amqp/evented-spec were born and became part of the amqp gem family of libraries. "300 one patch forks" hell is a thing of the past. Even initial pure Ruby implementation of the new serialization library is "several times as memory efficient":http://www.rabbitmq.com/blog/2011/03/01/ruby-amqp-benchmarks/. "Documentation guides":http://bit.ly/amqp-gem-docs response if very positive. amqp gem 0.8.0.RCs implement all but 1 RabbitMQ extensions as well as AMQP 0.9.1 spec features original gem never implemented fully. Many large users of the gem upgraded to 0.8.0.RCs. The future for amqp gem and its spin-offs is bright. h2. amqp gem 0.8.0 backwards compatibility policy amqp gem 0.8.0 is *not 100% backwards compatible*. That said, for most applications, upgrade path will be easy. Over 90% of the deprecated API classes & methods are still in place and will only be dropped in the 0.9.0 release. Plenty of incompatibilities between 0.6.x and 0.7.x series were ironed out in 0.8.0.RCs. Finally, this guide will explain what has changed, why, why it is the right thing to do and how you should adapt your application code. h2. Why developers should upgrade to 0.8.0 * Great documentation * amqp gem 0.8.0 RCs and later versions are actively maintained * Largest library users either already switched to amqp gem 0.8.0 RCs or are switching in the near future (2011) * AMQP 0.9.1 spec implementation: many other popular clients, for example the RabbitMQ Java client, has dropped support for AMQP 0.8 specification * RabbitMQ extensions * Several AMQP brokers, including RabbitMQ (by far the most popular from the maintainers team observation), are planning to drop AMQP 0.8 spec support * Applications that do not use depricated API features & behaviors will have very seamless upgrade path to the amqp gem 0.9.0 and beyond h2. AMQP protocol version change amqp gem before 0.8.0 (0.6.x, 0.7.x) series implemented (most of) AMQP 0.8 specification. amqp gem 0.8.0 implements AMQP 0.9.1 and thus *requires RabbitMQ version 2.0 or later*. See {file:docs/RabbitMQVersions.textile RabbitMQ versions} for more information about RabbitMQ versions support and how obtain up-todate packages for your operating system. amqp gem 0.8.0 and later versions implement AMQP 0.9.1 and thus *requires RabbitMQ version 2.0 or later* h2. Follow established AMQP terminology h3. require "mq" is deprecated Instead of the following:

require "amqp"
require "mq"

or

require "mq"

switch to

require "amqp"

mq.rb will be removed before 1.0 release. h3. MQ class is deprecated Please use {AMQP::Channel} instead. MQ class & its methods are implemented in amqp gem 0.8.x for backwards compatibility, but only for transition period (that will end after 0.9 release). Why is it deprecated? Because it was a poor name choice all along. Both mq.rb and MQ class step away from AMQP terminology and make 8 out of 10 engineers think it has something to do with AMQP queues (in fact, MQ should have been called Channel all along). No other AMQP client library we know of invents its own terminology when it comes to AMQP entities, and amqp gem shouldn't, too. MQ class and class methods that use implicit connection (MQ.queue and so on) will be removed before 1.0 release. h2. MQ class is now AMQP::Channel MQ class was renamed to AMQP::Channel to follow established {file:docs/AMQP091ModelExplained.textile AMQP 0.9.1 terminology}. Implicit per-thread channels are deprecated so code like this:

# amqp gem 0.6.x code style: connection is implicit, channel is implicit. Error handling & recovery are thus
# not possible.
# Deprecated, do not use.
MQ.queue("search.indexing")

# same for exchanges. Deprecated, do not use.
MQ.direct("services.imaging")
MQ.fanout("services.broadcast")
MQ.topic("services.weather_updates")


# connection object lets you define error handlers
connection = AMQP.connect(:vhost => "myapp/production", :host => "192.168.0.18")
# channek object lets you define error handlers and use multiple channels per application,
# for example, one per thread
channel    = AMQP::Channel.new(connection)
# AMQP::Channel#queue has unchanged otherwise
channel.queue("search.indexing")

# exchanges examples
channel.direct("services.imaging")
channel.fanout("services.broadcast")
channel.topic("services.weather_updates")

MQ.queue, MQ.direct, MQ.fanout and MQ.topic methods will be removed before 1.0 release. Use instance methods on AMQP::Channel (AMQP::Channel#queue, AMQP::Channel#direct, AMQP::Channel#default_exchange and so on) instead. h2. MQ::Queue is now AMQP::Queue MQ::Queue is now AMQP::Queue. All the methods from 0.6.x series are still available. MQ::Queue alias will be removed before 1.0 release. Please switch to AMQP::Queue. h2. MQ::Exchange is now AMQP::Exchange MQ::Exchange is now AMQP::Exchange. All the methods from 0.6.x series are still available. MQ::Exchange alias will be removed before 1.0 release. Please switch to AMQP::Exchange. h2. MQ::Header is now AMQP::Header MQ::Header is now AMQP::Header. If you code has any type checks or case matches on MQ::Header, it needs to change to AMQP::Header. h2. AMQP.error is deprecated Catch-all solutions for error handling are very difficult to use. Automatic recovery and fine-grained event handling is also not possible. amqp gem 0.8.0.RC14 and later includes fine-grained {file:docs/ErrorHandling.textile Error Handling and Recovery API} that is both significantly easier to use in real-world cases but also makes automatic recovery mode possible. AMQP.error method will be removed before 1.0 release. h2. MQ::RPC is deprecated It was an experiment that was never finished. Some API design choices are very opinionated as well. amqp gem should not ship with half-baked poorly designed or overly opinionated solutions. MQ::RPC class will be removed before 1.0 release. h2. MQ::Logger is removed It was an experiment that was never finished. Some API design choices are very opinionated as well. amqp gem should not ship with half-baked poorly designed or overly opinionated solutions. MQ::Logger class was removed in the the amqp gem 0.8.0 development cycle. h2. AMQP::Buffer is removed AMQP::Buffer was AMQP 0.8 protocol implementation detail. With the new "amq-protocol gem":http://rubygems.org/gems/amq-protocol, it is no longer relevant. AMQP::Buffer class was removed in the the amqp gem 0.8.0 development cycle. h2. AMQP::Frame is removed AMQP::Frame was AMQP 0.8 protocol implementation detail. With the new "amq-protocol gem":http://rubygems.org/gems/amq-protocol, it is no longer relevant. AMQP::Frame class was removed in the the amqp gem 0.8.0 development cycle. h2. AMQP::Server is removed AMQP::Server was (an unfinished) toy implementation of AMQP 0.8 broker in Ruby on top of EventMachine. We believe that Ruby is not an optimal choice for AMQP broker implementations. We also never heard from anyone using it. AMQP::Server class was removed in the the amqp gem 0.8.0 development cycle. h2. AMQP::Protocol::* classes are removed AMQP::Protocol::* classes were AMQP 0.8 protocol implementation detail. With the new "amq-protocol gem":http://rubygems.org/gems/amq-protocol, they are no longer relevant. AMQP::Protocol::* classes were removed in the the amqp gem 0.8.0 development cycle. h2. Authors This guide was written by "Michael Klishin":http://twitter.com/michaelklishin and edited by "Chris Duncan":https://twitter.com/celldee. h2. Tell us what you think! Please take a moment and tell us what you think about this guide "on Twitter":http://twitter.com/rubyamqp or "Ruby AMQP mailing list":http://groups.google.com/group/ruby-amqp: what was unclear? what wasn't covered? maybe you don't like guide style or grammar and spelling are incorrect? Readers feedback is key to making documentation better. If mailing list communication is not an option for you for some reason, you can "contact guides author directly":mailto:michael@novemberain.com?subject=amqp%20gem%20documentation amqp-1.8.0/docs/GettingStarted.textile0000644000004100000410000005722313321132064017772 0ustar www-datawww-data# @title Ruby amqp gem: Getting Started with AMQP and Ruby h1. Getting started with the Ruby amqp gem h2. This Documentation Has Moved to rubyamqp.info amqp gem documentation guides are now hosted on "rubyamqp.info":http://rubyamqp.info. h2. About this guide This guide is a quick tutorial that helps you to get started with v0.9.1 of the AMQP specification in general and the "Ruby amqp gem":http://github.com/ruby-amqp/amqp in particular. It should take about 20 minutes to read and study the provided code examples. This guide covers: * Installing RabbitMQ, a mature popular server implementation of the AMQP protocol. * Installing the amqp gem via "Rubygems":http://rubygems.org and "Bundler":http://gembundler.com. * Running a "Hello, world" messaging example that is a simple demonstration of 1:1 communication. * Creating a "Twitter-like" publish/subscribe example with 1 publisher and 4 subscribers that demonstrates 1:n communication. * Creating a topic routing example with 2 publishers and 8 subscribers showcasing n:m communication when subscribers only receive messages that they are interested in. * Learning how the amqp gem can be integrated with Ruby objects in a way that makes unit testing easy. This work is licensed under a Creative Commons Attribution 3.0 Unported License (including images & stylesheets). The source is available "on Github":https://github.com/ruby-amqp/amqp/tree/master/docs. h2. Which versions of the amqp gem does this guide cover? This guide covers v0.8.0 and later of the "Ruby amqp gem":https://github.com/ruby-amqp/amqp. h2. Installing RabbitMQ The "RabbitMQ site":http://rabbitmq.com has a good "installation guide":http://www.rabbitmq.com/install.html that addresses many operating systems. On Mac OS X, the fastest way to install RabbitMQ is with "Homebrew":http://mxcl.github.com/homebrew/:

brew install rabbitmq

then run it:

rabbitmq-server

On Debian and Ubuntu, you can either "download the RabbitMQ .deb package":http://www.rabbitmq.com/server.html and install it with "dpkg":http://www.debian.org/doc/FAQ/ch-pkgtools.en.html or make use of the "apt repository":http://www.rabbitmq.com/debian.html#apt that the RabbitMQ team provides. For RPM-based distributions like RedHat or CentOS, the RabbitMQ team provides an "RPM package":http://www.rabbitmq.com/install.html#rpm. The RabbitMQ package that ships with recent Ubuntu versions (for example, 10.10) is outdated and *will not work with v0.8.0 and later of the amqp gem* (we need at least RabbitMQ v2.0 for use with this guide). h2. Installing the Ruby amqp gem h3. Make sure that you have Ruby and "Rubygems":http://docs.rubygems.org/read/chapter/3 installed This guide assumes that you have installed one of the following supported Ruby implementations: * Ruby v1.8.7 [except for 1.8.7-p248 and -p249 that have "a bug that severely affects amqp gem":http://bit.ly/iONBmH] * Ruby v1.9.2 * Ruby v1.9.3 * JRuby (we recommend v1.6) * Rubinius v1.2 or higher * Ruby Enterprise Edition h3. You can use Rubygems to install the amqp gem h4. On Microsoft Windows 7:
gem install eventmachine --pre
gem install amqp
h4. On other OSes or JRuby:
gem install amqp
h3. You can also use Bundler to install the gem

source :rubygems

gem "amqp", "~> 0.9.0" # optionally: :git => "git://github.com/ruby-amqp/amqp.git", :branch => "0.9.x-stable"

h3. Verifying your installation Let us verify your installation with this quick irb session:

irb -rubygems

:001 > require "amqp"
=> true
:002 > AMQP::VERSION
=> "0.9.0"

h2. "Hello, world" example Let us begin with the classic "Hello, world" example. First, here is the code: (if the example above isn't displayed, see this "gist":https://gist.github.com/998690) This example demonstrates a very common communication scenario: *application A* wants to publish a message that will end up in a queue that *application B* listens on. In this case, the queue name is "amqpgem.examples.hello". Let us go through the code step by step:

require "rubygems"
require "amqp"

is the simplest way to load the amqp gem if you have installed it with RubyGems. The following piece of code

EventMachine.run do
  # ...
end

runs what is called the "EventMachine":http://rubyeventmachine.com reactor. We will not go into what the term 'reactor' means here, but suffice it to say that the amqp gem is asynchronous and is based on an asynchronous network I/O library called _EventMachine_. The next line

connection = AMQP.connect(:host => '127.0.0.1')

connects to the server running on localhost, with the default port (5672), username (guest), password (guest) and virtual host ('/'). The next line

channel  = AMQP::Channel.new(connection)

opens a new _channel_. AMQP is a multi-channeled protocol that uses channels to multiplex a TCP connection. Channels are opened on a connection, therefore the AMQP::Channel constructor takes a connection object as a parameter. This line

queue    = channel.queue("amqpgem.examples.helloworld", :auto_delete => true)

declares a _queue_ on the channel that we have just opened. Consumer applications get messages from queues. We declared this queue with the "auto-delete" parameter. Basically, this means that the queue will be deleted when there are no more processes consuming messages from it. The next line

exchange = channel.direct("")

instantiates an _exchange_. Exchanges receive messages that are sent by producers. Exchanges route messages to queues according to rules called _bindings_. In this particular example, there are no explicitly defined bindings. The exchange that we defined is known as the _default exchange_ and it has implied bindings to all queues. Before we get into that, let us see how we define a handler for incoming messages

queue.subscribe do |payload|
  puts "Received a message: #{payload}. Disconnecting..."
  connection.close { EventMachine.stop }
end

{AMQP::Queue#subscribe} takes a block that will be called every time a message arrives. {AMQP::Session#close} closes the AMQP connection and runs a callback that stops the EventMachine reactor. Finally, we publish our message

exchange.publish "Hello, world!", :routing_key => queue.name

Routing key is one of the _message attributes_. The default exchange will route the message to a queue that has the same name as the message's routing key. This is how our message ends up in the "amqpgem.examples.helloworld" queue. This first example can be modified to use the method chaining technique: This diagram demonstrates the "Hello, world" example data flow: !https://github.com/ruby-amqp/amqp/raw/master/docs/diagrams/001_hello_world_example_routing.png! For the sake of simplicity, both the message producer (App I) and the consumer (App II) are running in the same Ruby process. Now let us move on to a little bit more sophisticated example. h2. Blabbr: one-to-many publish/subscribe (pubsub) example The previous example demonstrated how a connection to a broker is made and how to do 1:1 communication using the default exchange. Now let us take a look at another common scenario: broadcast, or multiple consumers and one producer. A very well-known broadcast example is Twitter: every time a person tweets, followers receive a notification. Blabbr, our imaginary information network, models this scenario: every network member has a separate queue and publishes blabs to a separate exchange. 3 Blabbr members, Joe, Aaron and Bob, follow the official NBA account on Blabbr to get updates about what is happening in the world of basketball. Here is the code: The first line has a few differences from the "Hello, world" example above: * We use {AMQP.start} instead of {AMQP.connect} * Instead of return values, we pass a block to the connection method and it yields a connection object back as soon as the connection is established. * Instead of passing connection parameters as a hash, we use a URI string. {AMQP.start} is just a convenient way to do

EventMachine.run do
  AMQP.connect(options) do |connection|
    # ...
  end
end

The {AMQP.start} call blocks the current thread which means that its use is limited to scripts and small command line applications. Blabbr is just that. {AMQP.connect}, when invoked with a block, will yield a connection object as soon as the AMQP connection is open. Finally, connection parameters may be supplied as a Hash or as a connection string. The {AMQP.connect} method documentation contains all of the details. In this example, opening a channel is no different to opening a channel in the previous example, however, the exchange is declared differently

exchange = channel.fanout("nba.scores")

The exchange that we declare above using {AMQP::Channel#fanout} is a _fanout exchange_. A fanout exchange delivers messages to all of the queues that are bound to it: exactly what we want in the case of Blabbr. This piece of code

channel.queue("joe", :auto_delete => true).bind(exchange).subscribe do |payload|
  puts "#{payload} => joe"
end

is similar to the subscription code that we used for message delivery previously, but what does that {AMQP::Queue#bind} method do? It sets up a binding between the queue and the exchange that you pass to it. We need to do this to make sure that our fanout exchange routes messages to the queues of any subscribed followers.

exchange.publish("BOS 101, NYK 89").publish("ORL 85, ALT 88")

demonstrates {AMQP::Exchange#publish} call chaining. Blabbr members use a fanout exchange for publishing, so there is no need to specify a message routing key because every queue that is bound to the exchange will get its own copy of all messages, regardless of the queue name and routing key used. A diagram for Blabbr looks like this: !https://github.com/ruby-amqp/amqp/raw/master/docs/diagrams/002_blabbr_example_routing.png! Next we use EventMachine's {http://eventmachine.rubyforge.org/EventMachine.html#M000466 add_timer} method to run a piece of code in 2 seconds from now:

EventMachine.add_timer(2) do
  exchange.delete

  connection.close { EventMachine.stop }
end

The code that we want to run deletes the exchange that we declared earlier using {AMQP::Exchange#delete} and closes the AMQP connection with {AMQP::Session#close}. Finally, we stop the EventMachine event loop and exit. Blabbr is pretty unlikely to secure hundreds of millions of dollars in funding, but it does a pretty good job of demonstrating how one can use AMQP fanout exchanges to do broadcasting. h2. Weathr: many-to-many topic routing example So far, we have seen point-to-point communication and broadcasting. Those two communication styles are possible with many protocols, for instance, HTTP handles these scenarios just fine. You may ask "what differentiates AMQP?" Well, next we are going to introduce you to _topic exchanges_ and routing with patterns, one of the features that makes AMQP very powerful. Our third example involves weather condition updates. What makes it different from the previous two examples is that not all of the consumers are interested in all of the messages. People who live in Portland usually do not care about the weather in Hong Kong (unless they are visiting soon). They are much more interested in weather conditions around Portland, possibly all of Oregon and sometimes a few neighbouring states. Our example features multiple consumer applications monitoring updates for different regions. Some are interested in updates for a specific city, others for a specific state and so on, all the way up to continents. Updates may overlap so that an update for San Diego, CA appears as an update for California, but also should show up on the North America updates list. Here is the code: The first line that is different from the Blabbr example is

exchange = channel.topic("pub/sub", :auto_delete => true)

We use a topic exchange here. Topic exchanges are used for "multicast":http://en.wikipedia.org/wiki/Multicast messaging where consumers indicate which topics they are interested in (think of it as subscribing to a feed for an individual tag in your favourite blog as opposed to the full feed). Routing with a topic exchange is done by specifying a _routing pattern_ on binding, for example:

channel.queue("americas.south").bind(exchange, :routing_key => "americas.south.#").subscribe do |headers, payload|
  puts "An update for South America: #{payload}, routing key is #{headers.routing_key}"
end

Here we bind a queue with the name of "americas.south" to the topic exchange declared earlier using the {AMQP::Queue#bind} method. This means that only messages with a routing key matching "americas.south.#" will be routed to that queue. A routing pattern consists of several words separated by dots, in a similar way to URI path segments joined by slashes. Here are a few examples: * asia.southeast.thailand.bangkok * sports.basketball * usa.nasdaq.aapl * tasks.search.indexing.accounts Now let us take a look at a few routing keys that match the "americas.south.#" pattern: * americas.south * americas.south.*brazil* * americas.south.*brazil.saopaolo* * americas.south.*chile.santiago* In other words, the "#" part of the pattern matches 0 or more words. For a pattern like "americas.south.*", some matching routing keys would be: * americas.south.*brazil* * americas.south.*chile* * americas.south.*peru* but not * americas.south * americas.south.chile.santiago so "*" only matches a single word. The AMQP v0.9.1 specification says that topic segments (words) may contain the letters A-Z and a-z and digits 0-9. A (very simplistic) diagram to demonstrate topic exchange in action: !https://github.com/ruby-amqp/amqp/raw/master/docs/diagrams/003_weathr_example_routing.png! One more thing that is different from previous examples is that the block we pass to {AMQP::Queue#subscribe} now takes two arguments: a _header_ and a _body_ (often called the _payload_). Long story short, the header parameter lets you access metadata associated with the message. Some examples of message metadata attributes are: * message content type * message content encoding * message priority * message expiration time * message identifier * reply to (specifies which message this is a reply to) * application id (identifier of the application that produced the message) and so on. As the following binding demonstrates, "#" and "*" can also appear at the beginning of routing patterns:

channel.queue("us.tx.austin").bind(exchange, :routing_key => "#.tx.austin").subscribe do |headers, payload|
  puts "An update for Austin, TX: #{payload}, routing key is #{headers.routing_key}"
end

For this example the publishing of messages is no different from that of previous examples. If we were to run the program, a message published with a routing key of "americas.north.us.ca.berkeley" would be routed to 2 queues: "us.california" and the _server-named queue_ that we declared by passing a blank string as the name:

channel.queue("", :exclusive => true) do |queue|
  queue.bind(exchange, :routing_key => "americas.north.#").subscribe do |headers, payload|
    puts "An update for North America: #{payload}, routing key is #{headers.routing_key}"
  end
end

The name of the server-named queue is generated by the broker and sent back to the client with a queue declaration confirmation. Because the queue name is not known before the reply arrives, we pass {AMQP::Channel#queue} a callback and it yields us back a queue object once confirmation has arrived. h3. Avoid race conditions A word of warning: you may find examples on the Web of {AMQP::Channel#queue} usage that do not use callbacks. We *recommend that you use a callback for server-named queues*, otherwise your code may be subject to "race conditions":http://en.wikipedia.org/wiki/Race_condition. Even though the amqp gem tries to be reasonably smart and protect you from most common problems (for example, binding operations will be delayed until after queue name is received from the broker), there is no way it can do so for every case. The primary reason for supporting {AMQP::Channel#queue} usage without a callback for server-named queues is backwards compatibility with earlier versions. h2. Integration with objects Since Ruby is a genuine object-oriented language, it is important to demonstrate how the Ruby amqp gem can be integrated into rich object-oriented code. The {AMQP::Queue#subscribe} callback does not have to be a block. It can be any Ruby object that responds to `call`. A common technique is to combine {http://rubydoc.info/stdlib/core/1.8.7/Object:method Object#method} and {http://rubydoc.info/stdlib/core/1.8.7/Method:to_proc Method#to_proc} and use object methods as message handlers. An example to demonstrate this technique:

class Consumer

  #
  # API
  #

  def handle_message(metadata, payload)
    puts "Received a message: #{payload}, content_type = #{metadata.content_type}"
  end # handle_message(metadata, payload)
end


class Worker

  #
  # API
  #


  def initialize(channel, queue_name = AMQ::Protocol::EMPTY_STRING, consumer = Consumer.new)
    @queue_name = queue_name

    @channel    = channel
    @channel.on_error(&method(:handle_channel_exception))

    @consumer   = consumer
  end # initialize

  def start
    @queue = @channel.queue(@queue_name, :exclusive => true)
    @queue.subscribe(&@consumer.method(:handle_message))
  end # start



  #
  # Implementation
  #

  def handle_channel_exception(channel, channel_close)
    puts "Oops... a channel-level exception: code = #{channel_close.reply_code}, message = #{channel_close.reply_text}"
  end # handle_channel_exception(channel, channel_close)
end

The "Hello, world" example can be ported to use this technique: The most important line in this example is

@queue.subscribe(&@consumer.method(:handle_message))

Ampersand (&) preceding an object is equivalent to calling the #to_proc method on it. We obtain a Consumer#handle_message method reference with

@consumer.method(:handle_message)

and then the ampersand calls #to_proc on it. {AMQP::Queue#subscribe} then will be using this Proc instance to handle incoming messages. Note that the `Consumer` class above can be easily tested in isolation, without spinning up any AMQP connections. Here is one example using "RSpec":http://relishapp.com/rspec

require "ostruct"
require "json"

# RSpec example
describe Consumer do
  describe "when a new message arrives" do
    subject { described_class.new }

    let(:metadata) do
      o = OpenStruct.new

      o.content_type = "application/json"
      o
    end
    let(:payload)  { JSON.encode({ :command => "reload_config" }) }

    it "does some useful work" do
      # check preconditions here if necessary

      subject.handle_message(metadata, payload)

      # add your code expectations here
    end
  end
end

h2. Wrapping up This is the end of the tutorial. Congratulations! You have learned quite a bit about both AMQP v0.9.1 and the amqp gem. This is only the tip of the iceberg. AMQP has many more features built into the protocol: * Reliable delivery of messages * Message confirmations (a way to tell broker that a message was or was not processed successfully) * Message redelivery when consumer applications fail or crash * Load balancing of messages between multiple consumers * Message metadata attributes and so on. Other guides explain these features in depth, as well as use cases for them. To stay up to date with amqp gem development, "follow @rubyamqp on Twitter":http://twitter.com/rubyamqp and "join our mailing list":http://groups.google.com/group/ruby-amqp. h2. What to read next Documentation is organized as a number of {file:docs/DocumentationGuidesIndex.textile documentation guides}, covering all kinds of topics from {file:docs/Exchanges.textile use cases for various exchange types} to {file:docs/ErrorHandling.textile error handling} and {file:docs/VendorSpecificExchanges.textile Broker-specific AMQP 0.9.1 extensions}. We recommend that you read the following guides next, if possible, in this order: * {file:docs/AMQP091ModelExplained.textile AMQP 0.9.1 Model Explained}. A simple 2 page long introduction to the AMQP Model concepts and features. Understanding the AMQP Model will make a lot of other documentation, both for the Ruby amqp gem and RabbitMQ itself, easier to follow. With this guide, you don't have to waste hours of time reading the whole specification. * {file:docs/ConnectingToTheBroker.textile Connection to the broker}. This guide explains how to connect to an AMQP broker and how to integrate the amqp gem into standalone and Web applications. * {file:docs/Queues.textile Working With Queues}. This guide focuses on features that consumer applications use heavily. * {file:docs/Exchanges.textile Working With Exchanges}. This guide focuses on features that producer applications use heavily. * {file:docs/PatternsAndUseCases.textile Patterns & Use Cases}. This guide focuses implementation of "common messaging patterns":http://www.eaipatterns.com/ using AMQP Model features as building blocks. * {file:docs/ErrorHandling.textile Error Handling & Recovery}. This guide explains how to handle protocol errors, network failures and other things that may go wrong in real world projects. If you are migrating your application from earlier versions of the amqp gem (0.6.x and 0.7.x), to 0.8.x and later, there is the {file:docs/08Migration.textile amqp gem 0.8 migration guide}. h2. Authors This guide was written by "Michael Klishin":http://twitter.com/michaelklishin and edited by "Chris Duncan":https://twitter.com/celldee. h2. Tell us what you think! Please take a moment to tell us what you think about this guide "on Twitter":http://twitter.com/rubyamqp or the "Ruby AMQP mailing list":http://groups.google.com/group/ruby-amqp. Let us know what was unclear or what has not been covered. Maybe you do not like the guide style or grammar or discover spelling mistakes. Reader feedback is key to making the documentation better. If, for some reason, you cannot use the communication channels mentioned above, you can "contact the author of the guides directly":mailto:michael@novemberain.com?subject=amqp%20gem%20documentation
amqp-1.8.0/docs/AMQP091ModelExplained.textile0000644000004100000410000005062013321132064020637 0ustar www-datawww-data# @title Ruby amqp gem: AMQP 0.9.1 Model Explained h1. AMQP 0.9.1 Model Explained h2. This Documentation Has Moved to rubyamqp.info amqp gem documentation guides are now hosted on "rubyamqp.info":http://rubyamqp.info. h2. About this guide This guide explains the AMQP 0.9.1 Model used by RabbitMQ. Understanding the AMQP Model will make a lot of other documentation, both for the Ruby amqp gem and RabbitMQ itself, easier to follow. This work is licensed under a Creative Commons Attribution 3.0 Unported License (including images & stylesheets). The source is available "on Github":https://github.com/ruby-amqp/amqp/tree/master/docs. h3. What this guide covers This guide covers: * High-level overview of the AMQP 0.9.1 Model * Key differences of the AMQP model from some other messaging models * What exchanges are * What queues are * What bindings are * How AMQP protocol is structured and what AMQP methods are * What attributes AMQP 0.9.1 messages have * What message acknowledgements are * What negative message acknowledgements are * and a lot of other things h2. High-level overview of AMQP 0.9.1 and the AMQP Model h3. What is AMQP AMQP (Advanced Message Queuing Protocol) is a networking protocol which enables conforming client applications to communicate with conforming messaging middleware brokers. h3. Why AMQP was created Messaging solutions have been around since the 1970s with a view to solving the problem of integrating incompatible products from diverse vendors. Without the use of messaging middleware, the integration of heterogenous systems has proved to be very expensive and complex. However, messaging solutions, such as IBM Websphere MQ and Tibco Enterprise Message Service, are also very costly and tend to be exclusively employed by large companies (who can afford them), especially those in the financial services industry. There is also a problem with interoperability between messaging solutions. Vendors have created their own proprietary messaging protocols which do not interoperate with others, therefore resulting in 'vendor lock-in'. AMQP has multiple design goals but two of the most important are: * To produce an open standard for messaging middleware * To enable interoperability between various technologies and platforms There is a lot of software running on many operating systems built with multiple programming languages running on various hardware architectures and virtual machines. AMQP not only makes it possible for these disparate systems to communicate with one another, but also enables different products that implement AMQP to exchange information. h3. Brokers and their role Messaging brokers receive messages from _producers_ (applications that publish them) and route them to _consumers_ (applications that process them). If you imagine the human body, then brokers would be equivalent to centers of the nervous system and applications would be more like limbs. h3. AMQP 0.9.1 Model in brief The AMQP 0.9.1 Model has the following view of the world: messages are published by producers to _exchanges_, often compared to post offices or mailboxes. Exchanges then distribute message copies to _queues_ using rules called _bindings_. Then AMQP brokers either push messages to _consumers_ subscribed to queues, or consumers fetch/pull messages from queues on demand. !https://github.com/ruby-amqp/amqp/raw/master/docs/diagrams/001_hello_world_example_routing.png! When publishing a message, producers may specify various _message attributes_ (message metadata). Some of this metadata may be used by the broker, however, the rest of it is completely opaque to the broker and is only used by applications that receive the message. Networks are unreliable and applications may fail to process messages, therefore the AMQP Model has a notion of _message acknowledgements_: when a message is pushed down to a consumer, the consumer _notifies the broker_, either automatically or as soon as the application developer chooses to do so. When message acknowledgements are in use, a broker will only completely remove a message from a queue when it receives a notification for that message (or group of messages). In certain situations, for example, when a message cannot be routed, messages may be _returned_ to producers, dropped, or, if the broker implements an extension, placed into a so-called "dead letter queue". Producers choose how to handle situations like this by publishing messages using certain parameters. Queues, exchanges and bindings are commonly referred to as _AMQP entities_. h3. AMQP is a Programmable Protocol AMQP 0.9.1 is a programmable protocol in the sense that AMQP entities and routing schemes are defined by applications themselves, not a broker administrator. Accordingly, provision is made for protocol operations that declare queues and exchanges, define bindings between them, subscribe to queues and so on. This gives application developers a lot of freedom but also requires them to be aware of potential definition conflicts. In practice, definition conflicts are rare and often indicate misconfigurations. This can be very useful as it is a good thing if misconfigurations are caught early. Applications declare the AMQP entities that they need, define necessary routing schemes and may choose to delete AMQP entities when they are no longer used. h2. AMQP Exchanges and Exchange Types _Exchanges_ are AMQP entities where messages are sent. Exchanges then take a message and route it into one or more (or no) queues. The routing algorithm used depends on _exchange type_ and rules called _bindings_. AMQP 0.9.1 brokers typically provide 4 exchange types out of the box: * Direct exchange (typically used for for 1-to-1 communication or unicasting) * Fanout exchange (1-to-n communication or broadcasting) * Topic exchange (1-to-n or n-to-m communication, multicasting) * Headers exchange (message metadata-based routing) but it is possible to extend AMQP 0.9.1 brokers with custom exchange types, for example: * x-random exchange (randomly chooses a queue to route incoming messages to) * x-recent-history (a fanout exchange that also keeps N recent messages in memory) * regular expressions based variations of headers exchange and so on. Besides the type, exchanges have a number of attributes, most important of which are: * Name * Can be durable (information about them is persisted to disk and thus survives broker restarts) or non-durable (information is only kept in RAM) * Can have metadata associated with them on declaration h2. AMQP Queues Queues in the AMQP Model are very similar to queues in other message and "task queueing" systems: they store messages that are consumed by applications. Like AMQP exchanges, an AMQP queue has a name and a durability property but also * Can be exclusive (used by only one connection) * Can be automatically deleted when last consumer unsubscribes * Can have metadata associated with them on declaration (some brokers use it to implement features like message TTL) h2. AMQP Bindings Bindings are rules that exchanges use (among other things) to route messages to queues. To instruct an exchange E to route messages to a queue Q, Q has to _be bound_ to E. Bindings may have an optional _routing key_ attribute used by some exchange types. The purpose of the routing key is to selectively match only specific (matching) messages published to an exchange to the bound queue. In other words, the routing key acts like a filter. To draw an analogy: * Queue is like your destination in New York city * Exchange is like JFK airport * Bindings are routes from JFK to your destination. There can be none or more than one way to reach it Having this layer of indirection enables routing scenarios that are impossible of very hard to implement using publishing directly to queues and also eliminates certain amount of duplicated work application developers have to do. If AMQP message cannot be routed to any queue (for example, because there are no bindings for the exchange it was published to), it is either dropped or returned to the publisher, depending on message attributes the publisher has set. h2. AMQP Message Consumers Storing messages in queues is useless unless applications can _consume_ them. In the AMQP 0.9.1 Model, there are two ways for applications to do this: * Have messages pushed to them ("push API") * Fetch messages as needed ("pull API") With the "push API", applications have to indicate interest in consuming messages from a particular queue. When they do so, we say that they _register a consumer_ or, simply put, _subscribe to a queue_. It is possible to have more than one consumer per queue or to register an _exclusive consumer_ (excludes all other consumers from the queue while it is consuming). Each consumer (subscription) has an identifier called a _consumer tag_. It can be used to unsubscribe from messages. Consumer tags are just strings. h2. AMQP Message Attributes and Payload Messages in the AMQP Model have _attributes_. Some attributes are so common that the AMQP v0.9.1 specification defines them and application developers do not have to think about the exact attribute name. Some examples are * Content type * Content encoding * Routing key * Delivery mode (persistent or not) * Message priority * Message publishing timestamp * Expiration period * Producer application id Some attributes are used by AMQP brokers, but most are open to interpretation by applications that receive them. Some attributes are optional and known as _headers_. They are similar to X-Headers in HTTP. Message attributes are set when a message is published. AMQP messages also have a _payload_ (the data that they carry). Brokers treat this data as opaque (it is neither modified nor used by them). It is possible for messages to contain only attributes and no payload. It is common to use serialization formats like JSON, Thrift, Protocol Buffers and MessagePack to serialize structured data in order to publish it as AMQP message payload. h2. AMQP Message Acknowledgements Since networks are unreliable and applications fail, it is often necessary to have some kind of "processing acknowledgement". Sometimes it is only necessary to acknowledge the fact that a message has been received. Sometimes acknowledgements mean that a message was validated and processed by a consumer, for example, verified as having mandatory data and persisted to a data store or indexed. This situation is very common, so AMQP 0.9.1 has a built-in feature called _message acknowledgements_ (sometimes referred to as _acks_) that consumers use to confirm message delivery and/or processing. If an application crashes (AMQP broker notices this when connection is closed), if an acknowledgement for a message was expected but not received by the AMQP broker, the message is re-queued (and possibly immediately delivered to another consumer, if any exists). Having acknowledgements built into the protocol helps developers to build more robust software. h2. AMQP 0.9.1 Methods AMQP 0.9.1 is structured as a number of _methods_. Methods are operations (like HTTP methods) and have nothing in common with methods in object-oriented programming languages. AMQP methods are grouped into _classes_. Classes are just logical groupings of AMQP methods. The "AMQP 0.9.1 reference":http://www.rabbitmq.com/amqp-0-9-1-reference.html can be found on the RabbitMQ website. Let us take a look at the _exchange.*_ class, a group of methods related to operations on exchanges. It includes the following operations: * exchange.declare * exchange.declare-ok * exchange.delete * exchange.delete-ok (note that the RabbitMQ site reference also includes RabbitMQ-specific extensions to the exchange.* class that we will not discuss in this guide). The operations above form logical pairs: *exchange.declare* and *exchange.declare-ok*, *exchange.delete* and *exchange.delete-ok*. These operations are "requests" (sent by clients) and "responses" (sent by brokers in response to the aforementioned "requests"). As an example, the client asks the broker to declare a new exchange using the *exchange.declare* method: !https://img.skitch.com/20110720-c4qjdhmdrih9bn56npqnic4die.jpg! As shown on the diagram above, *exchange.declare* carries several _parameters_. They enable the client to specify exchange name, type, durability flag and so on. If the operation succeeds, the broker responds with the *exchange.declare-ok* method: !https://img.skitch.com/20110720-m4ptjbnex2sa52g6wdwj3e9ahm.jpg! *exchange.declare-ok* does not carry any parameters except for the channel number (channels will be described later in this guide). The sequence of events is very similar for another method pair, *queue.declare* and *queue.declare-ok*: !https://img.skitch.com/20110720-tmxswrie71ubb5m5nh8n17idhk.jpg! !https://img.skitch.com/20110720-g67urxg75c71qtwhwsjs684323.jpg! Not all AMQP methods have counterparts. Some (*basic.publish* being the most widely used one) do not have corresponding "response" methods and some others (*basic.get*, for example) have more than one possible "response". h2. AMQP Connections AMQP connections are typically long-lived. AMQP is an application level protocol that uses TCP for reliable delivery. AMQP connections use authentication and can be protected using TLS (SSL). When an application no longer needs to be connected to an AMQP broker, it should gracefully close the AMQP connection instead of abruptly closing the underlying TCP connection. h2. AMQP Channels Some applications need multiple connections to an AMQP broker. However, it is undesirable to keep many TCP connections open at the same time because doing so consumes system resources and makes it more difficult to configure firewalls. AMQP 0.9.1 connections are multiplexed with _channels_ that can be thought of as "lightweight connections that share a single TCP connection". For applications that use multiple threads/processes/etc for processing, it is very common to open a new channel per thread (process, etc.) and *not share* channels between them. Communication on a particular channel is completely separate from communication on another channel, therefore every AMQP method also carries a channel number that clients use to figure out which channel the method is for (and thus, which event handler needs to be invoked, for example). h2. AMQP Virtual Hosts (vhosts) To make it possible for a single broker to host multiple isolated "environments" (groups of users, exchanges, queues and so on), AMQP includes the concept of _virtual hosts_ (vhosts). They are similar to virtual hosts used by many popular Web servers and provide completely isolated environments in which AMQP entities live. AMQP clients specify what vhosts they want to use during AMQP connection negotiation. An AMQP 0.9.1 vhost name can be any non-blank string. Some most common use cases for vhosts are * To separate AMQP entities used by different groups of applications * To separate multiple installations/environments (e.g. production, staging) of one or more applications * To implement a multi-tenant environment h2. AMQP is Extensible AMQP 0.9.1 has several extension points: * Custom exchange types let developers implement routing schemes that exchange types provided out-of-the-box do not cover well, for example, geodata-based routing. * Declaration of exchanges and queues can include additional attributes that the broker can use. For example, per-queue message TTL in RabbitMQ is implemented this way. * Broker-specific extensions to the protocol. See, for example, "extensions RabbitMQ implements":http://www.rabbitmq.com/extensions.html. * New AMQP 0.9.1 method classes can be introduced. * Brokers can be extended with additional plugins, for example, RabbitMQ management frontend and HTTP API are implemented as a plugin. These features make the AMQP 0.9.1 Model even more flexible and applicable to a very broad range of problems. h2. Key differences from some other messaging models One key difference to understand about the AMQP 0.9.1 Model is that *messages are not sent to queues. They are sent to exchanges that route them to queues according to rules called "bindings"*. This means that routing is primarily handled by AMQP brokers and not applications themselves. TBD h2. AMQP 0.9.1 clients ecosystem h3. Overview There are many AMQP 0.9.1 clients for many popular programming languages and platforms. Some of them follow AMQP terminology closely and only provide implementation of AMQP methods. Some others have additional features, convenience methods and abstractions. Some of the clients are asynchronous (non-blocking), some are synchronous (blocking), some support both models. Some clients support vendor-specific extensions (for example, RabbitMQ-specific extensions). Because one of the main AMQP goals is interoperability, it is a good idea for developers to understand protocol operations and not limit themselves to terminology of a particular client library. This way communicating with developers using different libraries will be significantly easier. h2. Wrapping up This is the end of the AMQP 0.9.1 Model tutorial. Congratulations! Armed with this knowledge, you will find it easier to follow the rest of the amqp gem documentation as well as the rabbitmq.com documentation and the "rabbitmq-discuss mailing list":https://groups.google.com/forum/#!forum/rabbitmq-users To stay up to date with amqp gem development, "follow @rubyamqp on Twitter":http://twitter.com/rubyamqp and "join our mailing list":http://groups.google.com/group/ruby-amqp. h2. What to read next Documentation is organized as a number of {file:docs/DocumentationGuidesIndex.textile documentation guides}, covering all kinds of topics from {file:docs/Exchanges.textile use cases for various exchange types} to {file:docs/ErrorHandling.textile error handling} and {file:docs/VendorSpecificExchanges.textile Broker-specific AMQP 0.9.1 extensions}. We recommend that you read the following guides next, if possible, in this order: * {file:docs/ConnectingToTheBroker.textile Connection to the broker}. This guide explains how to connect to an AMQP broker and how to integrate the amqp gem into standalone and Web applications. * {file:docs/Queues.textile Working With Queues}. This guide focuses on features that consumer applications use heavily. * {file:docs/Exchanges.textile Working With Exchanges}. This guide focuses on features that producer applications use heavily. * {file:docs/PatternsAndUseCases.textile Patterns & Use Cases}. This guide focuses implementation of "common messaging patterns":http://www.eaipatterns.com/ using AMQP Model features as building blocks. * {file:docs/ErrorHandling.textile Error Handling & Recovery}. This guide explains how to handle protocol errors, network failures and other things that may go wrong in real world projects. If you are migrating your application from earlier versions of the amqp gem (0.6.x and 0.7.x), to 0.8.x and later, there is the {file:docs/08Migration.textile amqp gem 0.8 migration guide}. h2. Authors This guide was written by "Michael Klishin":http://twitter.com/michaelklishin and edited by "Chris Duncan":https://twitter.com/celldee. h2. Tell us what you think! Please take a moment to tell us what you think about this guide "on Twitter":http://twitter.com/rubyamqp or the "Ruby AMQP mailing list":http://groups.google.com/group/ruby-amqp. Let us know what was unclear or what has not been covered. Maybe you do not like the guide style or grammar or discover spelling mistakes. Reader feedback is key to making the documentation better. If, for some reason, you cannot use the communication channels mentioned above, you can "contact the author of the guides directly":mailto:michaelklishin@me.com?subject=amqp%20gem%20documentation
amqp-1.8.0/docs/Exchanges.textile0000644000004100000410000012672413321132064016752 0ustar www-datawww-data# @title Ruby AMQP gem: Working with exchanges h1. Working with exchanges h2. This Documentation Has Moved to rubyamqp.info amqp gem documentation guides are now hosted on "rubyamqp.info":http://rubyamqp.info. h2. About this guide This guide covers the use of exchanges according to the AMQP v0.9.1 specification including message publishing, common usage scenarios and how to accomplish typical operations using the Ruby amqp gem. This work is licensed under a Creative Commons Attribution 3.0 Unported License (including images & stylesheets). The source is available "on Github":https://github.com/ruby-amqp/amqp/tree/master/docs. h2. Which versions of the amqp gem does this guide cover? This guide covers v0.8.0 and later of the "Ruby amqp gem":http://github.com/ruby-amqp/amqp. h2. Exchanges in AMQP v0.9.1 - overview h3. What are AMQP exchanges? An _exchange_ accepts messages from a producer application and routes them to message queues. They can be thought of as the "mailboxes" of the AMQP world. Unlike some other messaging middleware products and protocols, in AMQP, messages are *not* published directly to queues. Messages are published to exchanges that route them to queue(s) using pre-arranged criteria called _bindings_. There are multiple exchange types in the AMQP v0.9.1 specification, each with its own routing semantics. Custom exchange types can be created to deal with sophisticated routing scenarios (e.g. routing based on geolocation data or edge cases) or just for convenience. h3. Concept of bindings A _binding_ is an association between a queue and an exchange. A queue must be bound to at least one exchange in order to receive messages from publishers. Learn more about bindings in the {file:docs/Bindings.textile Bindings guide}. h3. Exchange attributes Exchanges have several attributes associated with them: * Name * Type (direct, fanout, topic, headers or some custom type) * Durability * Whether the exchange is auto-deleted when no longer used * Other metadata (sometimes known as _X-arguments_) h2. Exchange types There are 4 built-in exchange types in AMQP v0.9.1: * Direct * Fanout * Topic * Headers As stated previously, each exchange type has its own routing semantics and new exchange types can be added by extending brokers with plugins. Custom exchange types begin with "x-", much like custom HTTP headers, e.g. "x-recent-history exchange":https://github.com/videlalvaro/rabbitmq-recent-history-exchange or "x-random exchange":https://github.com/jbrisbin/random-exchange. h2. Message attributes Before we start looking at various exchange types and their routing semantics, we need to introduce message attributes. Every AMQP message has a number of *attributes*. Some attributes are important and used very often, others are rarely used. AMQP message attributes are metadata and are similar in purpose to HTTP request and response headers. Every AMQP v0.9.1 message has an attribute called *routing key*. The routing key is an "address" that the exchange may use to decide how to route the message . This is similar to, but more generic than, a URL in HTTP. Most exchange types use the routing key to implement routing logic, but some ignore it and use other criteria (e.g. message content). h2. Fanout exchanges h3. How fanout exchanges route messages A fanout exchange routes messages to all of the queues that are bound to it and the routing key is ignored. If N queues are bound to a fanout exchange, when a new message is published to that exchange a *copy of the message* is delivered to all N queues. Fanout exchanges are ideal for the "broadcast routing":http://en.wikipedia.org/wiki/Broadcasting_%28computing%29 of messages. Graphically this can be represented as: !https://github.com/ruby-amqp/amqp/raw/master/docs/diagrams/004_fanout_exchange.png! h3. Declaring a fanout exchange There are two ways to declare a fanout exchange: * By instantiating an {AMQP::Exchange} and specifying the type as ":fanout" * By using the {AMQP::Channel#fanout} method Here are two examples to demonstrate:

exchange = AMQP::Exchange.new(channel, :fanout, "nodes.metadata")


exchange = channel.fanout("nodes.metadata")

Both methods asynchronously declare a queue. Because the declaration necessitates a network round-trip, publishing operations on {AMQP::Exchange} instances are delayed until the broker reply (`exchange.declare-ok`) is received. Also, both methods let you pass a block to run a piece of code when the broker responds with an `exchange.declare-ok` (meaning that the exchange has been successfully declared).

channel.fanout("nodes.metadata") do |exchange|
  # exchange is declared and ready to be used.
end

h3. Fanout routing example To demonstrate fanout routing behavior we can declare 10 server-named exclusive queues, bind them all to one fanout exchange and then publish a message to the exchange:

exchange = channel.topic("amqpgem.examples.routing.fanout_routing", :auto_delete => true)

10.times do
  q = channel.queue("", :exclusive => true, :auto_delete => true).bind(exchange)
  q.subscribe do |payload|
    puts "Queue #{q.name} received #{payload}"
  end
end


# Publish some test data after all queues are declared and bound
EventMachine.add_timer(1.2) { exchange.publish "Hello, fanout exchanges world!" }

When run, this example produces the following output:
Queue amq.gen-0p/BjxGNCue42RcJhpUrdg== received Hello, fanout exchanges world!
Queue amq.gen-3GXULvZuYh1KsOD83yvlNg== received Hello, fanout exchanges world!
Queue amq.gen-4EcyydTfoZzXjNSSLsh09Q== received Hello, fanout exchanges world!
Queue amq.gen-B1isyTpR5svB6ClQ2TQEBQ== received Hello, fanout exchanges world!
Queue amq.gen-FwLLioB7Mk4LGA4yJ1Mo7A== received Hello, fanout exchanges world!
Queue amq.gen-OtBQokiA/DmNkB5bPzaRig== received Hello, fanout exchanges world!
Queue amq.gen-RYHQUrj3yihb0DRF7KVpRg== received Hello, fanout exchanges world!
Queue amq.gen-SZJ40mGwbhdcbOGeHMhUkg== received Hello, fanout exchanges world!
Queue amq.gen-sDeVZg9Vx1knq+n9EMi8tA== received Hello, fanout exchanges world!
Queue amq.gen-uWOuVaosW4bWAHqKG6pZVw== received Hello, fanout exchanges world!
Each of the queues bound to the exchange receives a *copy* of the message. Full example: h3. Fanout use cases Because a fanout exchange delivers a copy of a message to every queue bound to it, its use cases are quite similar: * Massively multiplayer online (MMO) games can use it for leaderboard updates or other global events * Sport news sites can use fanout exchanges for distributing score updates to mobile clients in near real-time * Distributed systems can broadcast various state and configuration updates * Group chats can distribute messages between participants using a fanout exchange (although AMQP does not have a built-in concept of presence, so "XMPP":http://xmpp.org may be a better choice) h3. Pre-declared fanout exchanges AMQP v0.9.1 brokers must implement a fanout exchange type and pre-declare one instance with the name of "amq.fanout". Applications can rely on that exchange always being available to them. Each vhost has a separate instance of that exchange, it is *not shared across vhosts* for obvious reasons. h2. Direct exchanges h3. How direct exchanges route messages A direct exchange delivers messages to queues based on a *message routing key*, an attribute that every AMQP v0.9.1 message contains. Here is how it works: * A queue binds to the exchange with a routing key K * When a new message with routing key R arrives at the direct exchange, the exchange routes it to the queue if K = R A direct exchange is ideal for the "unicast routing":http://en.wikipedia.org/wiki/Unicast of messages (although they can be used for "multicast routing":http://en.wikipedia.org/wiki/Multicast as well). Here is a graphical representation: !https://github.com/ruby-amqp/amqp/raw/master/docs/diagrams/005_direct_exchange.png! h3. Declaring a direct exchange There are two ways to declare a direct exchange: * By instantiating a {AMQP::Exchange} and specifying its type as ":direct" * By using the {AMQP::Channel#direct} method Here are two examples to demonstrate:

exchange = AMQP::Exchange.new(channel, :direct, "nodes.metadata")


exchange = channel.direct("nodes.metadata")

Both methods asynchronously declare an exchange named "nodes.metadata". Because the declaration necessitates a network round trip, publishing operations on {AMQP::Exchange} instances are delayed until a broker reply (`exchange.declare-ok`) is received. Also, both methods let you pass a block to run a piece of code when the broker responds with `exchange.declare-ok` (meaning that the exchange has been successfully declared).

channel.direct("pages.content.extraction") do |exchange|
  # exchange is declared and ready to be used.
end

h3. Direct routing example Since direct exchanges use the *message routing key* for routing, message producers need to specify it:

exchange.publish("Hello, direct exchanges world!", :routing_key => "amqpgem.examples.queues.shared")

The routing key will then be compared for equality with routing keys on bindings, and consumers that subscribed with the same routing key each get a copy of the message: Full example: h3. Direct exchanges and load balancing of messages Direct exchanges are often used to distribute tasks between multiple workers (instances of the same application) in a round robin manner. When doing so, it is important to understand that, in AMQP v0.9.1, *messages are load balanced between consumers and not between queues*. The Ruby amqp gem historically has a limitation that only one consumer (message handler) is allowed per {AMQP::Queue} instance, however, this limitation will be addressed in the future. With the amqp gem v0.8.x, if you want to load balance messages between multiple consumers in the same application/OS process, then you need to use a separate channel for each of the consumers. The {file:docs/Queues.textile Working With Queues} and {file:docs/PatternsAndUseCases.textile Patterns and Use Cases} guides provide more information on this subject. h3. Pre-declared direct exchanges AMQP v0.9.1 brokers must implement a direct exchange type and pre-declare two instances: * *amq.direct* * *""* exchange known as *default exchange* (unnamed, referred to as an empty string by many clients including amqp Ruby gem) Applications can rely on those exchanges always being available to them. Each vhost has separate instances of those exchanges, they are *not shared across vhosts* for obvious reasons. h3. Default exchange The default exchange is a direct exchange with no name (the amqp gem refers to it using an empty string) pre-declared by the broker. It has one special property that makes it very useful for simple applications, namely that *every queue is automatically bound to it with a routing key which is the same as the queue name*. For example, when you declare a queue with the name of "search.indexing.online", the AMQP broker will bind it to the default exchange using "search.indexing.online" as the routing key. Therefore a message published to the default exchange with routing key = "search.indexing.online" will be routed to the queue "search.indexing.online". In other words, the default exchange makes it *seem like it is possible to deliver messages directly to queues*, even though that is not technically what is happening. The amqp gem offers two ways of obtaining a reference to the default exchange: * Using the {AMQP::Channel#default_exchange} method * Using the {AMQP::Channel#direct} method with an empty string as the exchange name {AMQP::Exchange#initialize} can also be used, but requires more coding effort and it offers no benefits over instance methods on {AMQP::Channel} in this particular case. Some examples of usage:

exchange = AMQP::Exchange.new(channel, :direct, "")


exchange = channel.default_exchange


exchange = channel.direct("")

The default exchange is used by the "Hello, World" example: Additionally, the routing example above can be rewritten to use the default exchange: h3. Direct exchange use cases Direct exchanges can be used in a wide variety of cases: * Direct (near real-time) messages to individual players in an MMO game * Delivering notifications to specific geographic locations (for example, points of sale) * Distributing tasks between multiple instances of the same application all having the same function, for example, image processors * Passing data between workflow steps, each having an identifier (also consider using headers exchange) * Delivering notifications to individual software services in the network h2. Topic exchanges h3. How topic exchanges route messages Topic exchanges route messages to one or many queues based on matching between a message routing key and the pattern that was used to bind a queue to an exchange. The topic exchange type is often used to implement various "publish/subscribe pattern":http://en.wikipedia.org/wiki/Publish/subscribe variations. Topic exchanges are commonly used for the "multicast routing":http://en.wikipedia.org/wiki/Multicast of messages. !http://upload.wikimedia.org/wikipedia/commons/thumb/3/30/Multicast.svg/500px-Multicast.svg.png! Topic exchanges can be used for "broadcast routing":http://en.wikipedia.org/wiki/Broadcasting_%28computing%29, but fanout exchanges are usually more efficient for this use case. h3. Topic exchange routing example Two classic examples of topic-based routing are stock price updates and location-specific data (for instance, weather broadcasts). Consumers indicate which topics they are interested in (think of it like subscribing to a feed for an individual tag of your favourite blog as opposed to the full feed). The routing is enabled by specifying a _routing pattern_ to the {AMQP::Queue#bind} method, for example:

channel.queue("americas.south").bind(exchange, :routing_key => "americas.south.#").subscribe do |headers, payload|
  puts "An update for South America: #{payload}, routing key is #{headers.routing_key}"
end

In the example above we bind a queue with the name of "americas.south" to the topic exchange declared earlier using the {AMQP::Queue#bind} method. This means that only messages with a routing key matching "americas.south.#" will be routed to the "americas.south" queue. A routing pattern consists of several words separated by dots, in a similar way to URI path segments being joined by slash. A few of examples: * asia.southeast.thailand.bangkok * sports.basketball * usa.nasdaq.aapl * tasks.search.indexing.accounts The following routing keys match the "americas.south.#" pattern: * americas.south * americas.south.*brazil* * americas.south.*brazil.saopaolo* * americas.south.*chile.santiago* In other words, the "#" part of the pattern matches 0 or more words. For the pattern "americas.south.*", some matching routing keys are: * americas.south.*brazil* * americas.south.*chile* * americas.south.*peru* but not * americas.south * americas.south.chile.santiago As you can see, the "*" part of the pattern matches 1 word only. Full example: h3. Topic exchange use cases Topic exchanges have a very broad set of use cases. Whenever a problem involves multiple consumers/applications that selectively choose which type of messages they want to receive, the use of topic exchanges should be considered. To name a few examples: * Distributing data relevant to specific geographic location, for example, points of sale * Background task processing done by multiple workers, each capable of handling specific set of tasks * Stocks price updates (and updates on other kinds of financial data) * News updates that involve categorization or tagging (for example, only for a particular sport or team) * Orchestration of services of different kinds in the cloud * Distributed architecture/OS-specific software builds or packaging where each builder can handle only one architecture or OS h2. Declaring/Instantiating exchanges With the Ruby amqp gem, exchanges can be declared in two ways: * By using the {AMQP::Exchange#initialize} method that takes an optional callback * By using a number of convenience methods on {AMQP::Channel} instances: ** {AMQP::Channel#direct} ** {AMQP::Channel#default_exchange} ** {AMQP::Channel#topic} ** {AMQP::Channel#fanout} ** {AMQP::Channel#headers} The previous sections on specific exchange types (direct, fanout, headers, etc.) provide plenty of examples of how these methods can be used. h2. Publishing messages To publish a message to an AMQP exchange, use {AMQP::Exchange#publish}:

exchange.publish("Some payload")

{AMQP::Exchange#publish} can accept any object that responds to the `to_s` method, not just string instances: The message payload is completely opaque to the library and is not modified in any way. h3. Data serialization You are encouraged to take care of data serialization before publishing (i.e. by using JSON, Thrift, Protocol Buffers or some other serialization library). Note that because AMQP is a binary protocol, text formats like JSON largely lose their advantage of being easy to inspect as data travels across the network, so consider using "BSON":http://bsonspec.org instead. A few popular options for data serialization are: * JSON: "json gem":https://rubygems.org/gems/json (part of standard Ruby library on Ruby 1.9) or "yajl-ruby":https://rubygems.org/gems/yajl-ruby (Ruby bindings to YAJL) * BSON: "bson gem":https://rubygems.org/gems/bson for JRuby (implemented as a Java extension) and "bson_ext":https://rubygems.org/bson_ext (C extension) for C-based Rubies * "Message Pack":http://msgpack.org has Ruby bindings but currently does not provide Java implementation for JRuby * XML: "Nokogiri":https://nokogiri.org is a swiss army knife for XML processing with Ruby, built on top of libxml2 * Protocol Buffers: "beefcake":https://github.com/bmizerany/beefcake * Thrift: "thrift-client":https://github.com/fauna/thrift_client h3. Message metadata AMQP messages have various metadata attributes that can be set when a message is published. Some of the attributes are well-known and mentioned in the AMQP v0.9.1 specification, others are specific to a particular application. Well-known attributes are listed here as options that {AMQP::Exchange#publish} takes: * :routing_key * :persistent * :immediate * :mandatory * :content_type * :content_encoding * :priority * :message_id * :correlation_id * :reply_to * :type * :user_id * :app_id * :timestamp * :expiration All other attributes can be added to a _headers table_ (in Ruby parlance, headers hash) that {AMQP::Exchange#publish} accepts as the ":headers" argument. An example to show how message metadata attributes are passed to {AMQP::Exchange#publish}:
:routing_key
Used for routing messages depending on the exchange type and configuration.
:persistent
When set to true, AMQP broker will persist message to disk.
:immediate
This flag tells the server how to react if the message cannot be routed to a queue consumer immediately. If this flag is set to true, the server will return an undeliverable message to the producer with a basic.return AMQP method. If this flag is set to false, the server will queue the message, but with no guarantee that it will ever be consumed.
:mandatory
This flag tells the server how to react if the message cannot be routed to a queue. If this flag is set to true, the server will return an unroutable message to the producer with a basic.return AMQP method. If this flag is set to false, the server silently drops the message.
:content_type
MIME content type of message payload. Has the same purpose/semantics as HTTP Content-Type header.
:content_encoding
MIME content encoding of message payload. Has the same purpose/semantics as HTTP Content-Encoding header.
:priority
Message priority, from 0 to 9.
:message_id
Message identifier as a string. If applications need to identify messages, it is recommended that they use this attribute instead of putting it into the message payload.
:reply_to
Commonly used to name a reply queue (or any other identifier that helps a consumer application to direct its response). Applications are encouraged to use this attribute instead of putting this information into the message payload.
:correlation_id
ID of the message that this message is a reply to. Applications are encouraged to use this attribute instead of putting this information into the message payload.
:type
Message type as a string. Recommended to be used by applications instead of including this information into the message payload.
:user_id
Sender's identifier. Note that RabbitMQ will check that the "value of this attribute is the same as username AMQP connection was authenticated with":http://www.rabbitmq.com/extensions.html#validated-user-id, it SHOULD NOT be used to transfer, for example, other application user ids or be used as a basis for some kind of Single Sign-On solution.
:app_id
Application identifier string, for example, "eventoverse" or "webcrawler"
:timestamp
Timestamp of the moment when message was sent, in seconds since the Epoch
:expiration
Message expiration specification as a string
:headers
Ruby hash of any additional attributes that the application needs. Nested hashes are supported.
It is recommended that application authors use well-known message attributes when applicable instead of relying on custom headers or placing information into the message body. For example, if your application messages have priority, publishing timestamp, type and content type, you should use the respective AMQP message attributes instead of reinventing the wheel. h3. Validated user_id In some scenarios it is useful for consumers to be able to know the identity of the user who published a message. RabbitMQ implements a feature known as "validated User ID":http://www.rabbitmq.com/extensions.html#validated-user-id. If this property is set by a publisher, its value must be the same as the name of the user used to open the connection. If the user-id property is not set, the publisher's identity is not validated and remains private. h3. Publishing callback and reliable delivery in distributed environments Sometimes it is convenient to execute an operation after publishing a message. For this, {AMQP::Exchange#publish} provides an optional callback. It is important to clear up some expectations of when exactly it is run and how it is related to topics of message persistence, delivery reliability and so on.

exchange.publish(payload, :persistent => true, :type => "reports.done") do
  # ...
end

A common expectation of the code above is that it is run after the message "has been sent", or even "has been delivered". Unfortunately, neither of these expectations can be met by the Ruby amqp gem alone. Message publishing happens in several steps: * {AMQP::Exchange#publish} takes a message and various metadata attributes * {AMQP::Exchange#publish} internally calls #to_s on the message argument to get message payload * Resulting payload is staged for writing * On the next event loop tick, data is transferred to the OS kernel using one of the underlying system calls ("epoll":http://en.wikipedia.org/wiki/Epoll, "kqueue":http://en.wikipedia.org/wiki/Kqueue and so on) or NIO channels (in the case of JRuby) * OS kernel buffers data before sending it * Network driver may also employ buffering As you can see, "when data is sent" is a complicated issue and while methods to flush buffers certainly exist on various platforms, doing so in a cross-platform way that *includes the JVM* (that EventMachine also runs on) is non-trivial. In addition, even flushing buffers does not guarantee that the data was received by the broker because it might have crashed while data was travelling down the wire. The only way to reliably know whether data was received by the broker or a peer application is to use message acknowledgements. This is how TCP works and this approach is proven to work at the enormous scale of the modern Internet. AMQP (the protocol) fully embraces this fact and the amqp gem follows. Given all of this, you may ask "when does the {AMQP::Exchange#publish} callback fire?" The answer is on the next event loop tick. By then the data is pushed down to the OS kernel. As far as the Ruby library is concerned, it is reasonably safe behavior. The AMQP::Exchange#publish callback is fired on the next event loop tick. Data is staged for delivery immediately. Applications MUST NOT assume that by the time the callback has fired, the data is guaranteed to leave the local machine networking stack, reach the AMQP broker or any peer applications that the message needs to be routed to. In cases when you cannot afford to lose a single message, AMQP v0.9.1 applications can use one (or a combination of) the following protocol features: * Publisher confirms (a RabbitMQ-specific extension to AMQP v0.9.1) * Publishing messages as immediate and/or mandatory * Transactions (these introduce noticeable overhead and have a relatively narrow set of use cases) A more detailed overview of the pros and cons of each option can be found in a "blog post that introduces Publisher Confirms extension":http://bit.ly/rabbitmq-publisher-confirms by the RabbitMQ team. The next sections of this guide will describe how the features above can be used with the Ruby amqp gem. h3. Publishing messages as immediate When publishing messages, it is possible to use the ":immediate" option to publish a message as "immediate". When an immediate message cannot be delivered to any consumer (meaning that one or more queues to which the message was routed have no active consumers), then the message is returned to the producer. An example of {AMQP::Exchange#publish} being used to publish an immediate message:

exchange.publish("Message ##{i}", :immediate => true)

The following code example demonstrates a message that is published as immediate but cannot be immediately consumed (no consumers) and thus is returned back to the producer: h3. Publishing messages as mandatory When publishing messages, it is possible to use the ":mandatory" option to publish a message as "mandatory". When a mandatory message cannot be *routed* to any queue (for example, there are no bindings or none of the bindings match), the message is returned to the producer. The following code example demonstrates a message that is published as mandatory but cannot be routed (no bindings) and thus is returned back to the producer: h3. Returned messages When a message is returned, the application that produced it can handle that message in different ways: * Store it for later redelivery in a persistent store * Publish it to a different destination * Log the event and discard the message Returned messages contain information about the exchange they were published to. For convenience, the amqp gem associates returned message callbacks with {AMQP::Exchange} instances. To handle returned messages, use {AMQP::Exchange#on_return}:

exchange.on_return do |basic_return, metadata, payload|
  puts "#{payload} was returned! reply_code = #{basic_return.reply_code}, reply_text = #{basic_return.reply_text}"
end

A returned message handler has access to AMQP method (basic.return) information, message metadata and payload. The metadata and message body are returned without modifications so that the application can store the message for later redelivery. h3. Publishing persistent messages Messages potentially spend some time in the queues to which they were routed before they are consumed. During this period of time, the broker may crash or experience a restart. To survive it, messages must be persisted to disk. This has a negative effect on performance, especially with network attached storage like NAS devices and Amazon EBS. AMQP v0.9.1 lets applications trade off performance for durability, or vice versa, on a message-by-message basis. To publish a persistent message, use the ":persistent" option that {AMQP::Exchange#publish} accepts:

exchange.publish(payload, :persistent => true)

Note that in order to survive a broker crash, both the message and the queue that it was routed to must be persistent/durable. {file:docs/Durability.textile Durability and Message Persistence} provides more information on the subject. h3. Publishing In Multi-threaded Environments When using amqp gem in multi-threaded environments, the rule of thumb is: avoid sharing {AMQP::Channel} instances across threads. Starting with 0.8.0.RC14, {AMQP::Exchange#publish} synchronizes data delivery on the channel object associated with exchange. This protects application developers from the most common problems related to publishing messages on a shared channel from multiple threads, however, by no means protects from every possible concurrency hazard. When using amqp gem in multi-threaded environments, the rule of thumb is: avoid sharing {AMQP::Channel} instances across threads. h3. Sending one-off messages The following example publishes a message and *safely* closes the AMQP connection afterwards by passing a block to {AMQP::Exchange#publish}: h2. Headers exchanges Now that message attributes and publishing have been introduced, it is time to take a look at one more core exchange type in AMQP v0.9.1. It is called the _headers_ exchange type and is quite powerful. h3. How headers exchanges route messages h4. An example problem definition The best way to explain headers-based routing is with an example. Imagine a distributed "continuous integration":http://martinfowler.com/articles/continuousIntegration.html system that distributes builds across multiple machines with different hardware architectures (x86, IA-64, AMD64, ARM family and so on) and operating systems. It strives to provide a way for a community to contribute machines to run tests on and a nice build matrix like "the one WebKit uses":http://build.webkit.org/waterfall?category=core. One key problem such systems face is build distribution. It would be nice if a messaging broker could figure out which machine has which OS, architecture or combination of the two and route build request messages accordingly. A headers exchange is designed to help in situations like this by routing on multiple attributes that are more easily expressed as message metadata attributes (headers) rather than a routing key string. h4. Routing on multiple message attributes Headers exchanges route messages based on message header matching. Headers exchanges ignore the routing key attribute. Instead, the attributes used for routing are taken from the "headers" attribute. When a queue is bound to a headers exchange, the ":arguments" attribute is used to define matching rules:

# when binding to a headers exchange, :arguments parameter is used to specify matching rules
@channel.queue("", :auto_delete => true).bind(exchange, :arguments => { :os => 'linux' })

When matching on one header, a message is considered matching if the value of the header equals the value specified upon binding. Using the example above, some messages that match would be:

exchange.publish "For linux/IA64",   :headers => { :arch => "IA64", :os => 'linux' }
exchange.publish "For linux/x86",    :headers => { :arch => "x86",  :os => 'linux' }
exchange.publish "For any linux",    :headers => { :os => 'linux' }

The following example demonstrates matching on integer values:

# consumer part
@channel.queue("", :auto_delete => true).bind(exchange, :arguments => { :cores => 8 })

# ...

# producer part
exchange.publish "For ocotocore", :headers => { :cores => 8 }

Matching on hashes (in AMQP v0.9.1 parlance - _attribute tables_) is also supported:

# consumer part
channel.queue("", :auto_delete => true).bind(exchange, :arguments => { :package => { :name => 'riak', :version => '0.14.2' } })

# ...

# producer part
exchange.publish "For nodes with Riak 0.14.2", :headers => { :package => { :name => 'riak', :version => '0.14.2' } }

h4. Matching all vs matching one It is possible to bind a queue to a headers exchange using more than one header for matching. In this case, the broker needs one more piece of information from the application developer, namely, should it consider messages with any of the headers matching, or all of them? This is what the "x-match" binding argument is for:

channel.queue("", :auto_delete => true).bind(exchange, :arguments => { 'x-match' => 'all', :arch => "ia64", :os => 'linux' })

In the example above, only messages that have an "arch" header value equal to "ia64" and an "os" header value equal to "linux" will be considered matching.

channel.queue("", :auto_delete => true).bind(exchange, :arguments => { 'x-match' => 'any', :os => 'macosx', :cores => 8 })

When the "x-match" argument is set to "any", just one matching header value is sufficient. So in the example above, any message with a "cores" header value equal to 8 will be considered matching. h4. More examples TBD h3. Declaring a headers exchange There are two ways to declare a headers exchange: * By instantiating {AMQP::Exchange} and specifying type as ":headers" * By using the {AMQP::Channel#headers} method Here are two examples to demonstrate:

exchange = AMQP::Exchange.new(channel, :headers, "builds")


exchange = channel.headers("builds")

Both methods asynchronously declare a queue. Because declaration necessitates a network round trip, publishing operations on {AMQP::Exchange} instances are delayed until the broker reply (`exchange.declare-ok`) is received. Both methods let you pass a block to run a piece of code when the broker responds with `exchange.declare-ok` (meaning that the exchange has been successfully declared).

channel.headers("builds") do |exchange|
  # exchange is declared and ready to be used.
end

h3. Headers exchange routing example When there is just one queue bound to a headers exchange, messages are routed to it if one or all headers of the message match those specified upon binding. Whether it is "any header" or "all of them" depends on the "x-match" header value. In the case of multiple queues, a headers exchange will deliver a copy of a message to each queue, just like direct exchanges do. Distribution rules between consumers on a particular queue are the same as for a direct exchange. Full example: h3. Headers exchange use cases Headers exchanges can be looked upon as "direct exchanges on steroids". Because they route based on header values, they can be used as direct exchanges where the routing key does not have to be a string; it could be an integer or a hash (dictionary) for example. Some specific use cases: * Transfer of work between stages in a multi-step workflow ("routing slip pattern":http://eaipatterns.com/RoutingTable.html) * Distributed build/continuous integration systems can distribute builds based on multiple parameters (OS, CPU architecture, availability of a particular package). h3. Pre-declared headers exchanges AMQP v0.9.1 brokers should (as defined by "IETF RFC 2119":http://www.ietf.org/rfc/rfc2119.txt) implement a headers exchange type and pre-declare one instance with the name of "amq.match". RabbitMQ also pre-declares one instance with the name of "amq.headers". Applications can rely on that exchange always being available to them. Each vhost has a separate instance of those exchanges and they are *not shared across vhosts* for obvious reasons. h2. Custom exchange types h3. x-random The "x-random AMQP exchange type":https://github.com/jbrisbin/random-exchange is a custom exchange type developed as a RabbitMQ plugin by Jon Brisbin. To quote from the project README: bq. It is basically a direct exchange, with the exception that, instead of each consumer bound to that exchange with the same routing key getting a copy of the message, the exchange type randomly selects a queue to route to. This plugin is licensed under "Mozilla Public License 1.1":http://www.mozilla.org/MPL/MPL-1.1.html, same as RabbitMQ. h3. x-recent-history The "x-recent-history AMQP exchange type":https://github.com/videlalvaro/rabbitmq-recent-history-exchange is a customer exchange type implemented as a RabbitMQ plugin by Alvaro Videla, one of the authors of "RabbitMQ in action":http://bit.ly/rabbitmq. This plugin is licensed under the "MIT license":https://github.com/videlalvaro/rabbitmq-recent-history-exchange/blob/master/LICENSE.md. h2. Using the Publisher Confirms extension to AMQP v0.9.1 Please refer to {file:docs/VendorSpecificExtensions.textile Vendor-specific extensions to AMQP 0.9.1 spec} h3. Message acknowledgements and their relationship to transactions and publisher confirms Consumer applications (applications that receive and process messages) may occasionally fail to process individual messages, or might just crash. Additionally, network issues might be experienced. This raises a question - "when should the AMQP broker remove messages from queues?" This topic is covered in depth in the {file:docs/Queues.textile Working With Queues} guide, including prefetching and examples. In this guide, we will only mention how message acknowledgements are related to AMQP transactions and the Publisher Confirms extension. Let us consider a publisher application (P) that communications with a consumer (C) using AMQP v0.9.1. Their communication can be graphically represented like this:
-----       -----       -----
|   |   S1  |   |   S2  |   |
| P | ====> | B | ====> | C |
|   |       |   |       |   |
-----       -----       -----
We have two network segments, S1 and S2. Each of them may fail. P is concerned with making sure that messages cross S1, while the broker (B) and C are concerned with ensuring that messages cross S2 and are only removed from the queue when they are processed successfully. Message acknowledgements cover reliable delivery over S2 as well as successful processing. For S1, P has to use transactions (a heavyweight solution) or the more lightweight Publisher Confirms, a RabbitMQ-specific extension. h2. Using AMQP transactions TBD h2. Binding queues to exchanges Queues are bound to exchanges using the {AMQP::Queue#bind} method. This topic is described in detail in the {file:docs/Queues.textile Working with queues} documentation guide. h2. Unbinding queues from exchanges Queues are unbound from exchanges using the {AMQP::Queue#unbind} method. This topic is described in detail in the {file:docs/Queues.textile Working with queues} documentation guide. h2. Deleting exchange h3. Explicitly deleting an exchange Exchanges are deleted using the {AMQP::Exchange#delete} method:

exchange.delete

{AMQP::Exchange#delete} takes an optional callback that is run when a `exchange.delete-ok` reply arrives from the broker.

exchange.delete do |delete_ok|
  # by now exchange is guaranteed to be deleted
end

h3. Auto-deleted exchanges Exchanges can be *auto-deleted*. To declare an exchange as auto-deleted, use the ":auto_delete" option on declaration:

exchange = AMQP::Exchange.new(channel, :direct, "nodes.metadata", :auto_delete => true)


exchange = channel.direct("nodes.metadata", :auto_delete => true)

Full example: TBD: explain when exchange is considered to be "no longer in use" h2. Objects as message producers. Since Ruby is a genuine object-oriented language, it is important to demonstrate how the Ruby amqp gem can be integrated into rich object-oriented code. This part of the guide focuses on exchanges and the problems/solutions concerning producer applications (applications that primarily generate and publish messages, as opposed to consumers that receive and process them). Full example: TBD h2. Exchange durability vs Message durability See {file:docs/Durability.textile Durability guide} h2. Error handling and recovery See {file:docs/ErrorHandling.textile Error handling and recovery guide} h2. Vendor-specific extensions related to exchanges See {file:docs/VendorSpecificExtensions.textile Vendor-specific Extensions guide} h2. What to read next Documentation is organized as several {file:docs/DocumentationGuidesIndex.textile documentation guides} that cover all kinds of topics. Guides related to this one are * {file:docs/Durability.textile Durability and message persistence} * {file:docs/Bindings.textile Bindings} * {file:docs/PatternsAndUseCases.textile Patterns and Use Cases} * {file:docs/Queues.textile Working With Queues} * {file:docs/ErrorHandling.textile Error handling and recovery} h2. Authors This guide was written by "Michael Klishin":http://twitter.com/michaelklishin and edited by "Chris Duncan":https://twitter.com/celldee. h2. Tell us what you think! Please take a moment to tell us what you think about this guide "on Twitter":http://twitter.com/rubyamqp or the "Ruby AMQP mailing list":http://groups.google.com/group/ruby-amqp. Let us know what was unclear or what has not been covered. Maybe you do not like the guide style or grammar or discover spelling mistakes. Reader feedback is key to making the documentation better. If, for some reason, you cannot use the communication channels mentioned above, you can "contact the author of the guides directly":mailto:michael@novemberain.com?subject=amqp%20gem%20documentation
amqp-1.8.0/docs/RunningTests.textile0000644000004100000410000000752713321132064017507 0ustar www-datawww-data# @title Ruby AMQP gem: Running tests h1. Running amqp gem test suite h2. This Documentation Has Moved to rubyamqp.info amqp gem documentation guides are now hosted on "rubyamqp.info":http://rubyamqp.info. h2. About this guide This guide is for people who want to contribute to amqp gem development. It has no relevant information for those who want to build applications using the library. This work is licensed under a Creative Commons Attribution 3.0 Unported License (including images & stylesheets). The source is available "on Github":https://github.com/ruby-amqp/amqp/tree/master/docs. h2. Covered versions Not applicable. h2. Canonical repository Since late 2010, the canonical amqp gem repository is at "github.com/ruby-amqp/amqp":https://github.com/ruby-amqp/amqp and *not tmm1/amqp*. h2. Initial setup amqp gem currently uses recent releases of RabbitMQ for automated tests. Broker installation is described in the {file:docs/GettingStarted.textile Getting started} guide and on "rabbitmq.com":http://www.rabbitmq.com/install.html. Once the broker is running, the only other step is to run

./bin/set_test_suite_realms_up.sh
that will set up vhosts and users we test authentication and authorization against. Please note that the script uses `rabbitmqctl` tool and in the server environment (for example, a virtual instance in the cloud) depending on your OS and/or distribution of choice, that tool may or may not be accessible to your OS user, so pay attention to that. h2. Dependencies Dependencies are managed by Bundler. So begin with

gem install bundler
and then, from the root of the repository run

bundle install
and you are good to go. h2. Running spec examples During development, it is sufficient to just use

bundle exec rspec -c ./spec
or replace ./spec with an individual file you want to run. h2. Continuous integration CI happens in two steps: * Clean up *.rbc files produced by "Rubinius":http://rubini.us. * Run the whole test suite For that, we use Rake:

rake spec:ci
Thanks to the excellent "Travis CI":http://travis-ci.org project, we run test suites across multiple Ruby implementations/versions and 2 EventMachine versions. h2. Authors This guide was written by "Michael Klishin":http://twitter.com/michaelklishin and edited by "Chris Duncan":https://twitter.com/celldee. h2. Tell us what you think! Please take a moment and tell us what you think about this guide "on Twitter":http://twitter.com/rubyamqp or "Ruby AMQP mailing list":http://groups.google.com/group/ruby-amqp: what was unclear? what wasn't covered? maybe you don't like guide style or grammar and spelling are incorrect? Readers feedback is key to making documentation better. If mailing list communication is not an option for you for some reason, you can "contact guides author directly":mailto:michael@novemberain.com?subject=amqp%20gem%20documentation
amqp-1.8.0/docs/PatternsAndUseCases.textile0000644000004100000410000002441513321132064020716 0ustar www-datawww-data# @title Ruby AMQP gem: Patterns and Use Cases h1. Patterns and Use Cases h2. This Documentation Has Moved to rubyamqp.info amqp gem documentation guides are now hosted on "rubyamqp.info":http://rubyamqp.info. h2. About this guide This guide explains typical messaging patterns and use cases. It only covers the most common scenarios. For comprehensive list of messaging patterns, consult books on this subject, for example, "Enterprise Integration Patterns":http://www.eaipatterns.com. This work is licensed under a Creative Commons Attribution 3.0 Unported License (including images & stylesheets). The source is available "on Github":https://github.com/ruby-amqp/amqp/tree/master/docs. h2. Covered versions This guide covers "Ruby amqp gem":http://github.com/ruby-amqp/amqp v0.8.0 and later. h2. Introduction Messaging patterns are a lot like object-oriented design patterns: they are generalized reusable solutions to specific problems. They are not recipes, however, and their exact implementation may and will vary from application to application. Just like OO design patterns, they can classified: * Message construction patterns describe form, content and purpose of messages. * Message routing patterns outline how messages can be directed from producers to consumers. * Message transformation patterns change message content or metadata. There are other, more specialized group of messaging patterns that are out of scope of this guide. This guide demonstrates implementation of several common routing patterns plus explains how built-in AMQP 0.9.1 features can be used to implement message construction and message transformation patterns. Note that guide is a work in progress. There are many messaging patterns and new variations are being discovered every year. This guide thus strives to be useful to the 80% of developers instead of being "complete". h2. Request/Reply pattern h3. Description & Use cases Request/Reply is a simple way of integration when one application issues a request and another application responds to it. This pattern is often referred to as"Remote Procedure Call", even when it is not entirely correct. Request/Reply pattern is a 1:1 communication pattern. Some examples of Request/Reply pattern are: * The 1st application requests a document that the 2nd application generates or loads and returns. * End-user application issues a search request and another application returns results back. * One application requests a progress report from another application. h3. AMQP-based implementation Implementation of Request/Reply pattern on top of AMQP 0.9.1 involves two messages: a request (Req) and a response (Res). Client app generates a request identifier and sets :message_id attribute on Req. Client also uses a server-named exclusive queue to receive replies and thus sets :reply_to Req attribute to the name of that queue. Server app uses a well-known queue name to receive requests and sets :correlation_id to :message_id of the original request message (Req) to make it possible for the client to identify what request this reply is for. h4. Request message attributes
:message_id
Unique message identifier
:reply_to
Queue name server should send the response to
h4. Response message attributes
:correlation_id
Identifier of the original request message (set to request's :correlation_id)
:routing_key
Client's replies queue name (set to request's :reply_to)
h3. Code example h4. Client code h4. Server code In the examples above messages are published with the :immediate attribute set. This is not necessary in all cases: sometimes it is OK for requests to sit in the queue without active consumers. Replies, on the other hand, assume an active consumer and existing replies queue, so if routing or immediate delivery do not succeed, server application will log returned messages. More on this in the {file:docs/Exchanges.textile Working With Exchanges} guide. h3. Related patterns Request/Reply demonstrates two common techniques that are sometimes referred to as messaging patterns of its own: * "Correlation Identifier":http://www.eaipatterns.com/CorrelationIdentifier.html (for identifying what request incoming response is for) * "Return Address":http://www.eaipatterns.com/ReturnAddress.html (for identifying where replies should be sent) Other related patterns are * Scatter/Gather * Smart Proxy h2. Command pattern h3. Description & Use cases Command pattern is very similar to Request/Reply, except that there is no reply and messages are typed. For example, most modern Web applications have at least one "background task processor" that carries out a number of operations asynchronously, without sending any responses back. Command pattern usually assumes 1:1 communication. Some specific examples of Command pattern are: * Account termination in a Web app triggers information archiving (or deletion) that is done by a separate app "in the background". * After a document or profile update, a Web app sends out commands to a search indexer application. * Virtual machines control dashboard app sends virtual machine controller application a command to reboot. h3. AMQP-based implementation Implementation of Command pattern on top of AMQP 0.9.1 involves well-known durable queues. Application that issues the command then can use default exchange to publish messages to well-known services directly. Request message :type attribute then indicates command type and message body (or body and headers) carry additional information consumer needs to carry it out. h4. Request message attributes
:type
Message type as a string. For example: gems.install or commands.shutdown
h3. Code example h4. Producer (Sender) h4. Consumer (Recipient) h3. Related patterns * Event * Request/Reply h2. Event pattern h3. Description & Use cases Event pattern is a version of the Command pattern, but with 1 or more receivers (1:N communication). The world we live in is full of events, so applications of this pattern are endless. Some specific use cases of Event pattern are * Event logging (one application asks event collector to record certain event and possibly take action) * Event propagation in MMO games * Live sport score updates * Various "push notifications" for mobile applications The Event pattern is very similar to the Command pattern, however, there is typically certain differences between the two: * Event listeners often do not respond back to event producers * Event listeners are often concerned with data collection: they update counters, persist event information and so on * There may be more than event listener in the system. Commands are often carried out by one particular application h3. AMQP-based implementation Because Event pattern is a 1:N communication pattern, it typically uses a fanout exchange. Event listeners then use server-named exclusive queues and all bind to that exchange. Event messages use :type message attribute to indicate event type and message body (plus, possibly, message headers) to pass event context information. h4. Request message attributes
:type
Message type as a string. For example: files.created, files.indexed or pages.viewed
Due to misconfiguration or different upgrade time/policy, applications may receive events they do not know how to handle. It is important for developers to handle such cases, otherwise it is likely that consumers may crash. More on fanout exchange type in the {file:docs/Exchanges.textile Working With Exchanges} guide. h3. Code example h4. Producer (Sender) h4. Consumer (Handler) h3. Related patterns * Command * Publish/Subscribe h2. Document Message pattern h3. Description & Use cases Document Message pattern is very similar to Command and Event patterns. The difference is in the intent: whereas a Command message tells the receiver to invoke certain behavior, a Document Message just passes data and lets the receiver decide what, if anything, to do with the data. Message payload is a single logical entity, for example, one (or a group of closely related) database rows or documents. Use cases for the Document Message pattern often have something to do with processing of documents: * Indexing * Archiving * Content extraction * Transformation (translation, transcoding and so on) of document data h2. Competing Consumers pattern h3. Description & Use cases "Competing Consumers":http://www.eaipatterns.com/CompetingConsumers.html are multiple consumers that process messages from a shared queue. TBD h3. AMQP-based implementation TBD h3. Code example TBD h2. Publish/Subscribe pattern h3. Description & Use cases TBD h3. AMQP-based implementation TBD h3. Code example TBD h2. Scatter/Gather pattern h3. Description & Use cases TBD h3. AMQP-based implementation TBD h3. Code example TBD h2. Smart Proxy pattern h3. Description & Use cases TBD h3. AMQP-based implementation TBD h3. Code example TBD h2. Multistep Processing (Routing Slip) pattern h3. Description & Use cases TBD h3. AMQP-based implementation TBD h3. Code example TBD h2. Authors These guides were written by "Michael Klishin":http://twitter.com/michaelklishin and edited by "Chris Duncan":https://twitter.com/celldee h2. Tell us what you think! Please take a moment and tell us what you think about this guide "on Twitter":http://twitter.com/rubyamqp or "Ruby AMQP mailing list":http://groups.google.com/group/ruby-amqp: what was unclear? what wasn't covered? maybe you don't like guide style or grammar and spelling are incorrect? Readers feedback is key to making documentation better. If mailing list communication is not an option for you for some reason, you can "contact guides author directly":mailto:michael@novemberain.com?subject=amqp%20gem%20documentation amqp-1.8.0/docs/Durability.textile0000644000004100000410000001263213321132064017145 0ustar www-datawww-data# @title Ruby AMQP gem: Durability and related matters h1. Durability and related matters h2. This Documentation Has Moved to rubyamqp.info amqp gem documentation guides are now hosted on "rubyamqp.info":http://rubyamqp.info. h2. About this guide This guide covers queue, exchange and message durability, as well as other topics related to durability, for example, durability in cluster environment. This work is licensed under a Creative Commons Attribution 3.0 Unported License (including images & stylesheets). The source is available "on Github":https://github.com/ruby-amqp/amqp/tree/master/docs. h2. Covered versions This guide covers "Ruby amqp gem":http://github.com/ruby-amqp/amqp v0.8.0 and later. h2. Entity durability and message persistence h3. Durability of exchanges AMQP separates concept of durability of entities (queues, exchanges) from messages persistence. Exchanges can be durable or transient. Durable exchanges survive broker restart, transient exchanges don't (they have to be redeclared when broker comes back online). Not all scenarios and use cases mandate exchanges to be durable. h3. Durability of queues Durable queues are persisted to disk and thus survive broker restarts. Queues that are not durable are called transient. Not all scenarios and use cases mandate queues to be durable. Note that *only durable queues can be bound to durable exchanges*. This guarantees that it is possible to restore bindings on broker restart. Durability of a queue does not make _messages_ that are routed to that queue durable. If broker is taken down and then brought back up, durable queue will be re-declared during broker startup, however, only _persistent_ messages will be recovered. h3. Persistence of messages The concept of messages persistence is separate: messages may be published as persistent. That makes AMQP broker persist them to disk. If the server is restarted, the system ensures that received persistent messages are not lost. Simply publishing message to a durable exchange or the fact that queue(s) they are routed to is durable doesn't make messages persistent: it all depends on persistence mode of the messages itself. Publishing messages as persistent affects performance (just like with data stores, durability comes at a certain cost in performance and vise versa). Pass :persistent => true to {AMQP::Exchange#publish} to publish your message as persistent. h3. Transactions TBD h3. Publisher confirms Because transactions carry certain (for some applications, significant) overhead, RabbitMQ introduced an extension to AMQP 0.9.1 called {http://www.rabbitmq.com/blog/2011/02/10/introducing-publisher-confirms/ publisher confirms} ({http://www.rabbitmq.com/extensions.html#confirms documentation}). amqp gem implements support for this extension, but it is not loaded by default when you require "amqp". To load it, use

require "amqp/extensions/rabbitmq"

and then define a callback for publisher confirms using {AMQP::Channel#confirm}:

# enable publisher acknowledgements for this channel
channel.confirm_select

# define a callback that will be executed when message is acknowledged
channel.on_ack do |basic_ack|
  puts "Received an acknowledgement: delivery_tag = #{basic_ack.delivery_tag}, multiple = #{basic_ack.multiple}"
end

# define a callback that will be executed when message is rejected using basic.nack (a RabbitMQ-specific extension)
channel.on_nack do |basic_nack|
  puts "Received a nack: delivery_tag = #{basic_nack.delivery_tag}, multiple = #{basic_nack.multiple}"
end

Note that the same callback is used for all messages published via all exchanges on the given channel. h3. Clustering To achieve degree of durability critical applications need, it's necessary but not enough to use durable queues, exchanges and persistent messages. You need to use a cluster of brokers because otherwise, a single hardware problem may bring broker down completely. See {file:docs/Clustering.textile Clustering guide} for in-depth discussion of this topic. h2. Tell us what you think! Please take a moment and tell us what you think about this guide "on Twitter":http://twitter.com/rubyamqp or "Ruby AMQP mailing list":http://groups.google.com/group/ruby-amqp: what was unclear? what wasn't covered? maybe you don't like guide style or grammar and spelling are incorrect? Readers feedback is key to making documentation better. If mailing list communication is not an option for you for some reason, you can "contact guides author directly":mailto:michael@novemberain.com?subject=amqp%20gem%20documentation
amqp-1.8.0/docs/Queues.textile0000644000004100000410000012640613321132064016311 0ustar www-datawww-data# @title Ruby AMQP gem: Working with queues h1. Working with queues h2. This Documentation Has Moved to rubyamqp.info amqp gem documentation guides are now hosted on "rubyamqp.info":http://rubyamqp.info. h2. About this guide This guide covers everything related to queues in the AMQP v0.9.1 specification, common usage scenarios and how to accomplish typical operations using the amqp gem. This work is licensed under a Creative Commons Attribution 3.0 Unported License (including images & stylesheets). The source is available "on Github":https://github.com/ruby-amqp/amqp/tree/master/docs. h2. Which versions of the amqp gem does this guide cover? This guide covers v0.8.0 and later of the "Ruby amqp gem":http://github.com/ruby-amqp/amqp. h2. Queues in AMQP v0.9.1 - overview h3. What are AMQP queues? _Queues_ store and forward messages to consumers. They are similar to mailboxes in SMTP. Messages flow from producing applications to {file:docs/Exchanges.textile exchanges} that route them to queues and finally queues deliver the messages to consumer applications (or consumer applications fetch messages as needed). Note that unlike some other messaging protocols/systems, messages are not delivered directly to queues. They are delivered to exchanges that route messages to queues using rules known as _bindings_. AMQP is a programmable protocol, so queues and bindings alike are declared by applications. h3. Concept of bindings A _binding_ is an association between a queue and an exchange. Queues must be bound to at least one exchange in order to receive messages from publishers. Learn more about bindings in the {file:docs/Bindings.textile Bindings guide}. h3. Queue attributes Queues have several attributes associated with them: * Name * Exclusivity * Durability * Whether the queue is auto-deleted when no longer used * Other metadata (sometimes called _X-arguments_) These attributes define how queues can be used, what their life-cycle is like and other aspects of queue behavior. The amqp gem represents queues as instances of {AMQP::Queue}. h2. Queue names and declaring queues Every AMQP queue has a name that identifies it. Queue names often contain several segments separated by a dot ".", in a similar fashion to URI path segments being separated by a slash "/", although almost any string can represent a segment (with some limitations - see below). Before a queue can be used, it has to be *declared*. Declaring a queue will cause it to be created if it does not already exist. The declaration will have no effect if the queue does already exist and its attributes are the *same as those in the declaration*. When the existing queue attributes are not the same as those in the declaration a channel-level exception is raised. This case is explained later in this guide. h3. Explicitly named queues Applications may pick queue names or ask the broker to generate a name for them. To declare a queue with a particular name, for example, "images.resize", pass it to the Queue class constructor:

queue = AMQP::Queue.new(channel, "images.resize", :auto_delete => true)

Full example: h3. Server-named queues To ask an AMQP broker to generate a unique queue name for you, pass an *empty string* as the queue name argument:

AMQP::Queue.new(channel, "", :auto_delete => true) do |queue, declare_ok|
  puts "#{queue.name} is ready to go. AMQP method: #{declare_ok.inspect}"
end

Full example: The amqp gem allows server-named queues to be declared without callbacks:

queue = AMQP::Queue.new(channel, "", :auto_delete => true)

In this case, as soon as the AMQP broker reply (`queue.declare-ok` AMQP method) arrives, the queue object name will be assigned to the value that the broker generated. Many AMQP operations require a queue name, so before an {AMQP::Queue} instance receives its name, those operations are delayed. This example demonstrates this:

queue = channel.queue("")
queue.bind("builds").subscribe do |metadata, payload|
  # message handling implementation...
end

In this example, binding will be performed as soon as the queue has received its name generated by the broker. If a particular piece of code relies on the queue name being available immediately a callback should be used. h3. Reserved queue name prefix Queue names starting with "amq." are reserved for internal use by the broker. Attempts to declare a queue with a name that violates this rule will result in a channel-level exception with reply code 403 (ACCESS_REFUSED) and a reply message similar to this:
ACCESS_REFUSED - queue name 'amq.queue' contains reserved prefix 'amq.*'
h3. Queue re-declaration with different attributes When queue declaration attributes are different from those that the queue already has, a channel-level exception with code 406 (PRECONDITION_FAILED) will be raised. The reply text will be similar to this:
PRECONDITION_FAILED - parameters for queue 'amqpgem.examples.channel_exception' in vhost '/' not equivalent
h2. Queue life-cycle patterns According to the AMQP v0.9.1 specification, there are two common message queue life-cycle patterns: * Durable message queues that are shared by many consumers and have an independent existence: i.e. they will continue to exist and collect messages whether or not there are consumers to receive them. * Temporary message queues that are private to one consumer and are tied to that consumer. When the consumer disconnects, the message queue is deleted. There are some variations of these, such as shared message queues that are deleted when the last of many consumers disconnects. Let us examine the example of a well-known service like an event collector (event logger). A logger is usually up and running regardless of the existence of services that want to log anything at a particular point in time. Other applications know which queues to use in order to communicate with the logger and can rely on those queues being available and able to survive broker restarts. In this case, explicitly named durable queues are optimal and the coupling that is created between applications is not an issue. Another example of a well-known long-lived service is a distributed metadata/directory/locking server like "Apache Zookeeper":http://zookeeper.apache.org, "Google's Chubby":http://labs.google.com/papers/chubby.html or DNS. Services like this benefit from using well-known, not server-generated, queue names and so do any other applications that use them. A different sort of scenario is in "a cloud setting" when some kind of worker/instance might start and stop at any time so that other applications cannot rely on it being available. In this case, it is possible to use well-known queue names, but a much better solution is to use server-generated, short-lived queues that are bound to topic or fanout exchanges in order to receive relevant messages. Imagine a service that processes an endless stream of events - Twitter is one example. When traffic increases, development operations may start additional application instances in the cloud to handle the load. Those new instances want to subscribe to receive messages to process, but the rest of the system does not know anything about them and cannot rely on them being online or try to address them directly. The new instances process events from a shared stream and are the same as their peers. In a case like this, there is no reason for message consumers not to use queue names generated by the broker. In general, use of explicitly named or server-named queues depends on the messaging pattern that your application needs. {http://www.eaipatterns.com/ Enterprise Integration Patterns} discusses many messaging patterns in depth and the RabbitMQ FAQ also has a section on {http://www.rabbitmq.com/faq.html#scenarios use cases}. h2. Declaring a durable shared queue To declare a durable shared queue, you pass a queue name that is a non-blank string and use the ":durable" option:

queue = AMQP::Queue.new(channel, "images.resize", :durable => true)

Full example: the same example rewritten to use {AMQP::Channel#queue}:

channel.queue("images.resize", :durable => true) do |queue, declare_ok|
  puts "#{queue.name} is ready to go."
end

h2. Declaring a temporary exclusive queue To declare a server-named, exclusive, auto-deleted queue, pass "" (empty string) as the queue name and use the ":exclusive" and ":auto_delete" options:

AMQP::Queue.new(channel, "", :auto_delete => true, :exclusive => true) do |queue, declare_ok|
  puts "#{queue.name} is ready to go."
end

Full example: The same example can be rewritten to use {AMQP::Channel#queue}:

channel.queue("", :auto_delete => true, :exclusive => true) do |queue, declare_ok|
  puts "#{queue.name} is ready to go."
end

Full example: Exclusive queues may only be accessed by the current connection and are deleted when that connection closes. The declaration of an exclusive queue by other connections is not allowed and will result in a channel-level exception with the code 405 (RESOURCE_LOCKED) and a reply message similar to
RESOURCE_LOCKED - cannot obtain exclusive access to locked queue 'amqpgem.examples.queue' in vhost '/'
The following example demonstrates this: h2. Binding queues to exchanges In order to receive messages, a queue needs to be bound to at least one exchange. Most of the time binding is explcit (done by applications). To bind a queue to an exchange, use {AMQP::Queue#bind} where the argument passed can be either an {AMQP::Exchange} instance or a string.

queue.bind(exchange) do |bind_ok|
  puts "Just bound #{queue.name} to #{exchange.name}"
end

Full example: The same example using a string without callback:

queue.bind("amq.fanout")

Full example: h2. Subscribing to receive messages ("push API") To set up a queue subscription to enable an application to receive messages as they arrive in a queue, one uses the {AMQP::Queue#subscribe} method. Then when a message arrives, the message header (metadata) and body (payload) are passed to the handler:

queue.subscribe do |metadata, payload|
  puts "Received a message: #{payload.inspect}."
end

Full example: Subscriptions for message delivery are usually referred to as _consumers_ in the AMQP v0.9.1 specification, client library documentation and books. Consumers last as long as the channel that they were declared on, or until the client cancels them (unsubscribes). Consumers are identified by consumer tags. If you need to obtain the consumer tag of a subscribed queue then use {AMQP::Queue#consumer_tag}. h3. Accessing message metadata The `header` object in the example above provides access to message metadata and delivery information: * Message content type * Message content encoding * Message routing key * Message delivery mode (persistent or not) * Consumer tag this delivery is for * Delivery tag * Message priority * Whether or not message is redelivered * Producer application id and so on. An example to demonstrate how to access some of those attributes:

# producer
exchange.publish("Hello, world!",
                 :app_id      => "amqpgem.example",
                 :priority    => 8,
                 :type        => "kinda.checkin",
                 # headers table keys can be anything
                 :headers     => {
                   :coordinates => {
                     :latitude  => 59.35,
                     :longitude => 18.066667
                   },
                   :participants => 11,
                   :venue        => "Stockholm"
                 },
                 :timestamp   => Time.now.to_i)


# consumer
queue.subscribe do |metadata, payload|
  puts "metadata.routing_key : #{metadata.routing_key}"
  puts "metadata.content_type: #{metadata.content_type}"
  puts "metadata.priority    : #{metadata.priority}"
  puts "metadata.headers     : #{metadata.headers.inspect}"
  puts "metadata.timestamp   : #{metadata.timestamp.inspect}"
  puts "metadata.type        : #{metadata.type}"
  puts "metadata.delivery_tag: #{metadata.delivery_tag}"
  puts "metadata.redelivered : #{metadata.redelivered?}"

  puts "metadata.app_id      : #{metadata.app_id}"
  puts "metadata.exchange    : #{metadata.exchange}"
  puts
  puts "Received a message: #{payload}."
end

Full example: h3. Exclusive consumers Consumers can request exclusive access to the queue (meaning only this consumer can access the queue). This is useful when you want a long-lived shared queue to be temporarily accessible by just one application (or thread, or process). If the application employing the exclusive consumer crashes or loses the TCP connection to the broker, then the channel is closed and the exclusive consumer is cancelled. To exclusively receive messages from the queue, pass the ":exclusive" option to {AMQP::Queue#subscribe}:

queue.subscribe(:exclusive => true) do |metadata, payload|
  # message handling logic...
end

TBD: describe what happens when exclusivity property is violated and how to handle it. h3. Using multiple consumers per queue Historically, amqp gem versions before v0.8.0.RC14 (current master branch in the repository) have had a "one consumer per Queue instance" limitation. Previously, to work around this problem, application developers had to open multiple channels and work with multiple queue instances on different channels. This is not very convenient and is surprising for developers familiar with AMQP clients for other languages. With more and more Ruby implementations dropping the "GIL":http://en.wikipedia.org/wiki/Global_Interpreter_Lock, load balancing between multiple consumers in the same queue in the same OS process has become more and more common. In certain cases, even applications that do not need any concurrency benefit from having multiple consumers on the same queue in the same process. Starting from amqp gem v0.8.0.RC14, it is possible to add any number of consumers by instantiating {AMQP::Consumer} directly:

# non-exclusive consumer, consumer tag is generated
consumer1 = AMQP::Consumer.new(channel, queue)

# non-exclusive consumer, consumer tag is explicitly given
consumer2 = AMQP::Consumer.new(channel, queue, "#{queue.name}-consumer-#{rand}-#{Time.now}")

# exclusive consumer, consumer tag is generated
consumer3 = AMQP::Consumer.new(channel, queue, nil, true)

Instantiated consumers do not begin consuming messages immediately. This is because in certain cases, it is useful to add a consumer but make it active at a later time. To consume messages, use the {AMQP::Consumer#consume} method in combination with {AMQP::Consumer#on_delivery}:

consumer1.consume.on_delivery do |metadata, payload|
  @consumer1_mailbox << payload
end

{AMQP::Consumer#on_delivery} takes a block that is used exactly like the block passed to {AMQP::Queue#subscribe}. In fact, {AMQP::Queue#subscribe} uses {AMQP::Consumer} under the hood, adding a _default consumer_ to the queue. Default consumers do not have any special properties, they just provide a convenient way for application developers to register multiple consumers and a means of preserving backwards compatibility. Application developers are always free to use AMQP::Consumer instances directly, or intermix them with AMQP::Queue#subscribe. Most of the public API methods on {AMQP::Consumer} return self, so it is possible to use method chaining extensively. An example from "amqp gem spec suite":https://github.com/ruby-amqp/amqp/tree/master/spec:

consumer1 = AMQP::Consumer.new(@channel, @queue).consume.on_delivery { |metadata, payload| mailbox1 << payload }
consumer2 = AMQP::Consumer.new(@channel, @queue).consume.on_delivery { |metadata, payload| mailbox2 << payload }

To cancel a particular consumer, use {AMQP::Consumer#cancel} method. To cancel a default queue consumer, use {AMQP::Queue#unsubscribe}. h3. Message acknowledgements Consumer applications - applications that receive and process messages - may occasionally fail to process individual messages, or will just crash. There is also the possibility of network issues causing problems. This raises a question - "When should the AMQP broker remove messages from queues?" The AMQP v0.9.1 specification proposes two choices: * After broker sends a message to an application (using either basic.deliver or basic.get-ok methods). * After the application sends back an acknowledgement (using basic.ack AMQP method). The former choice is called the *automatic acknowledgement model*, while the latter is called the *explicit acknowledgement model*. With the explicit model, the application chooses when it is time to send an acknowledgement. It can be right after receiving a message, or after persisting it to a data store before processing, or after fully processing the message (for example, successfully fetching a Web page, processing and storing it into some persistent data store). !https://github.com/ruby-amqp/amqp/raw/master/docs/diagrams/006_amqp_091_message_acknowledgements.png! If a consumer dies without sending an acknowledgement, the AMQP broker will redeliver it to another consumer, or, if none are available at the time, the broker will wait until at least one consumer is registered for the same queue before attempting redelivery. The acknowledgement model is chosen when a new consumer is registered for a queue. By default, {AMQP::Queue#subscribe} will use the *automatic* model. To switch to the *explicit* model, the ":ack" option should be used:

queue.subscribe(:ack => true) do |metadata, payload|
  # message handling logic...
end

To demonstrate how redelivery works, let us have a look at the following code example: So what is going on here? This example uses 3 AMQP connections to imitate 3 applications, 1 producer and two consumers. Each AMQP connection opens a single channel:

# open three connections to imitate three apps
connection1 = AMQP.connect
connection2 = AMQP.connect
connection3 = AMQP.connect

channel_exception_handler = Proc.new { |ch, channel_close| EventMachine.stop; raise "channel error: #{channel_close.reply_text}" }

# open two channels
channel1    = AMQP::Channel.new(connection1)
channel1.on_error(&channel_exception_handler)
# ...

channel2    = AMQP::Channel.new(connection2)
channel2.on_error(&channel_exception_handler)
# ...

# app 3 will just publish messages
channel3    = AMQP::Channel.new(connection3)
channel3.on_error(&channel_exception_handler)

The consumers share a queue and the producer publishes messages to the queue periodically using an `amq.direct` exchange. Both "applications" subscribe to receive messages using the explicit acknowledgement model. The AMQP broker by default will send each message to the next consumer in sequence (this kind of load balancing is known as *round-robin*). This means that some messages will be delivered to consumer #1 and some to consumer #2.

exchange = channel3.direct("amq.direct")

# ...

queue1    = channel1.queue("amqpgem.examples.acknowledgements.explicit", :auto_delete => false)
# purge the queue so that we do not get any redeliveries from previous runs
queue1.purge
queue1.bind(exchange).subscribe(:ack => true) do |metadata, payload|
  # do some work
  sleep(0.2)

  # acknowledge some messages, they will be removed from the queue
  if rand > 0.5
    # FYI: there is a shortcut, metadata.ack
    channel1.acknowledge(metadata.delivery_tag, false)
    puts "[consumer1] Got message ##{metadata.headers['i']}, ack-ed"
  else
    # odd messages are not ack-ed and will remain in the queue for redelivery
    # when app #1 connection is closed (either properly or due to a crash)
    puts "[consumer1] Got message ##{metadata.headers['i']}, SKIPPED"
  end
end

queue2    = channel2.queue!("amqpgem.examples.acknowledgements.explicit", :auto_delete => false)
queue2.subscribe(:ack => true) do |metadata, payload|
  metadata.ack
  # app 2 always acks messages
  puts "[consumer2] Received #{payload}, redelivered = #{metadata.redelivered}, ack-ed"
end

To demonstrate message redelivery we make consumer #1 randomly select which messages to acknowledge. After 4 seconds we disconnect it (to imitate a crash). When that happens, the AMQP broker redelivers unacknowledged messages to consumer #2 which acknowledges them unconditionally. After 10 seconds, this example closes all outstanding connections and exits. An extract of output produced by this example:
=> Subscribing for messages using explicit acknowledgements model

[consumer2] Received Message #0, redelivered = false, ack-ed
[consumer1] Got message #1, SKIPPED
[consumer1] Got message #2, SKIPPED
[consumer1] Got message #3, ack-ed
[consumer2] Received Message #4, redelivered = false, ack-ed
[consumer1] Got message #5, SKIPPED
[consumer2] Received Message #6, redelivered = false, ack-ed
[consumer2] Received Message #7, redelivered = false, ack-ed
[consumer2] Received Message #8, redelivered = false, ack-ed
[consumer2] Received Message #9, redelivered = false, ack-ed
[consumer2] Received Message #10, redelivered = false, ack-ed
[consumer2] Received Message #11, redelivered = false, ack-ed
----- Connection 1 is now closed (we pretend that it has crashed) -----
[consumer2] Received Message #5, redelivered = true, ack-ed
[consumer2] Received Message #1, redelivered = true, ack-ed
[consumer2] Received Message #2, redelivered = true, ack-ed
[consumer2] Received Message #12, redelivered = false, ack-ed
[consumer2] Received Message #13, redelivered = false, ack-ed
[consumer2] Received Message #14, redelivered = false, ack-ed
[consumer2] Received Message #15, redelivered = false, ack-ed
[consumer2] Received Message #16, redelivered = false, ack-ed
[consumer2] Received Message #17, redelivered = false, ack-ed
[consumer2] Received Message #18, redelivered = false, ack-ed
[consumer2] Received Message #19, redelivered = false, ack-ed
[consumer2] Received Message #20, redelivered = false, ack-ed
[consumer2] Received Message #21, redelivered = false, ack-ed
[consumer2] Received Message #22, redelivered = false, ack-ed
[consumer2] Received Message #23, redelivered = false, ack-ed
[consumer2] Received Message #24, redelivered = false, ack-ed
[consumer2] Received Message #25, redelivered = false, ack-ed
[consumer2] Received Message #26, redelivered = false, ack-ed
[consumer2] Received Message #27, redelivered = false, ack-ed
[consumer2] Received Message #28, redelivered = false, ack-ed
[consumer2] Received Message #29, redelivered = false, ack-ed
[consumer2] Received Message #30, redelivered = false, ack-ed
[consumer2] Received Message #31, redelivered = false, ack-ed
[consumer2] Received Message #32, redelivered = false, ack-ed
[consumer2] Received Message #33, redelivered = false, ack-ed
[consumer2] Received Message #34, redelivered = false, ack-ed
[consumer2] Received Message #35, redelivered = false, ack-ed
As we can see, consumer #1 did not acknowledge 3 messages (labelled 1, 2 and 5):
[consumer1] Got message #1, SKIPPED
[consumer1] Got message #2, SKIPPED
...
[consumer1] Got message #5, SKIPPED
and then, once consumer #1 had "crashed", those messages were immediately redelivered to the consumer #2:
Connection 1 is now closed (we pretend that it has crashed)
[consumer2] Received Message #5, redelivered = true, ack-ed
[consumer2] Received Message #1, redelivered = true, ack-ed
[consumer2] Received Message #2, redelivered = true, ack-ed
To acknowledge a message use {AMQP::Channel#acknowledge}:

channel1.acknowledge(metadata.delivery_tag, false)

{AMQP::Channel#acknowledge} takes two arguments: message *delivery tag* and a flag that indicates whether or not we want to acknowledge multiple messages at once. Delivery tag is simply a channel-specific increasing number that the server uses to identify deliveries. When acknowledging multiple messages at once, the delivery tag is treated as "up to and including". For example, if delivery tag = 5 that would mean "acknowledge messages 1, 2, 3, 4 and 5". As a shortcut, it is possible to acknowledge messages using the {AMQP::Header#ack} method:

queue2.subscribe(:ack => true) do |metadata, payload|
  metadata.ack
end

Acknowledgements are channel-specific. Applications must not receive messages on one channel and acknowledge them on another. A message MUST not be acknowledged more than once. Doing so will result in a channel-level exception (PRECONDITION_FAILED) with an error message like this: «PRECONDITION_FAILED - unknown delivery tag» h3. Rejecting messages When a consumer application receives a message, processing of that message may or may not succeed. An application can indicate to the broker that message processing has failed (or cannot be accomplished at the time) by rejecting a message. When rejecting a message, an application can ask the broker to discard or requeue it. To reject a message use the {AMQP::Channel#reject} method:

queue.bind(exchange).subscribe do |metadata, payload|
  # reject but do not requeue (simply discard)
  channel.reject(metadata.delivery_tag)
end

in the example above, messages are rejected without requeueing (broker will simply discard them). To requeue a rejected message, use the second argument that {AMQP::Channel#reject} takes:

queue.bind(exchange).subscribe do |metadata, payload|
  # reject and requeue
  channel.reject(metadata.delivery_tag, true)
end

When there is only one consumer on a queue, make sure you do not create infinite message delivery loops by rejecting and requeueing a message from the same consumer over and over again. Another way to reject a message is by using {AMQP::Header#reject}:

queue.bind(exchange).subscribe do |metadata, payload|
  # reject but do not requeue (simply discard)
  metadata.reject
end


queue.bind(exchange).subscribe do |metadata, payload|
  # reject and requeue
  metadata.reject(:requeue => true)
end

h3. Negative acknowledgements Messages are rejected with the `basic.reject` AMQP method. There is one limitation that `basic.reject` has: there is no way to reject multiple messages, as you can do with acknowledgements. However, if you are using "RabbitMQ":http://rabbitmq.com, then there is a solution. RabbitMQ provides an AMQP v0.9.1 extension known as "negative acknowledgements":http://www.rabbitmq.com/extensions.html#negative-acknowledgements (nacks) and the amqp gem supports this extension. For more information, please refer to the {file:docs/VendorSpecificExtensions.textile Vendor-specific Extensions guide}. h3. QoS - Prefetching messages For cases when multiple consumers share a queue, it is useful to be able to specify how many messages each consumer can be sent at once before sending the next acknowledgement. This can be used as a simple load balancing technique or to improve throughput if messages tend to be published in batches. For example, if a producing application sends messages every minute because of the nature of the work it is doing. Imagine a website that takes data from social media sources like Twitter or Facebook during the Champions League final (or the Superbowl), and then calculates how many tweets mention a particular team during the last minute. The site could be structured as 3 applications: * A crawler that uses streaming APIs to fetch tweets/statuses, normalizes them and sends them in JSON for processing by other applications ("app A"). * A calculator that detects what team is mentioned in a message, updates statistics and pushes an update to the Web UI once a minute ("app B"). * A Web UI that fans visit to see the stats ("app C"). In this imaginary example, the "tweets per second" rate will vary, but to improve the throughput of the system and to decrease the maximum number of messages that the AMQP broker has to hold in memory at once, applications can be designed in such a way that application "app B", the "calculator", receives 5000 messages and then acknowledges them all at once. The broker will not send message 5001 unless it receives an acknowledgement. In AMQP parlance this is know as *QoS* or *message prefetching*. Prefetching is configured on a per-channel (typically) or per-connection (rarely used) basis. To configure prefetching per channel, use the {AMQP::Channel#prefetch} method. Let us return to the example we used in the "Message acknowledgements" section:

# app #1 will be given up to 3 messages at a time. If it does not
# send an ack after receiving the messages, then the messages will
# be routed to app #2.
channel1.prefetch(3)

# app #2 processes messages one-by-one and has to send an ack after receiving each message
channel2.prefetch(1)

In that example, one consumer prefetches 3 messages and another consumer prefetches just 1. If we take a look at the output that the example produces, we will see that `consumer1` fetched 4 messages and acknowledged 1. After that, all subsequent messages were delivered to `consumer2`:
[consumer2] Received Message #0, redelivered = false, ack-ed
[consumer1] Got message #1, SKIPPED
[consumer1] Got message #2, SKIPPED
[consumer1] Got message #3, ack-ed
[consumer2] Received Message #4, redelivered = false, ack-ed
[consumer1] Got message #5, SKIPPED
---
  by now consumer 1 has received 3 messages it did not acknowledge.
  With prefetch = 3, AMQP broker will not send it any more messages until consumer 1 sends an ack
---
[consumer2] Received Message #6, redelivered = false, ack-ed
[consumer2] Received Message #7, redelivered = false, ack-ed
[consumer2] Received Message #8, redelivered = false, ack-ed
[consumer2] Received Message #9, redelivered = false, ack-ed
[consumer2] Received Message #10, redelivered = false, ack-ed
[consumer2] Received Message #11, redelivered = false, ack-ed
The prefetching setting is ignored for consumers that do not use explicit acknowledgements. h2. How message acknowledgements relate to transactions and Publisher Confirms In cases where you cannot afford to lose a single message, AMQP v0.9.1 applications can use one or a combination of the following protocol features: * Publisher confirms (a RabbitMQ-specific extension to AMQP v0.9.1) * Publishing messages as immediate * Transactions (noticeable overhead) This topic is covered in depth in the {file:docs/Exchanges.textile Working With Exchanges} guide. In this guide, we will only mention how message acknowledgements are related to AMQP transactions and the Publisher Confirms extension. Let us consider a publisher application (P) that communications with a consumer (C) using AMQP v0.9.1. Their communication can be graphically represented like this:
-----       -----       -----
|   |   S1  |   |   S2  |   |
| P | ====> | B | ====> | C |
|   |       |   |       |   |
-----       -----       -----
We have two network segments, S1 and S2. Each of them may fail. P is concerned with making sure that messages cross S1, while broker (B) and C are concerned with ensuring that messages cross S2 and are only removed from the queue when they are processed successfully. Message acknowledgements cover reliable delivery over S2 as well as successful processing. For S1, P has to use transactions (a heavyweight solution) or the more lightweight Publisher Confirms RabbitMQ extension. h2. Fetching messages when needed ("pull API") The AMQP v0.9.1 specification also provides a way for applications to fetch (pull) messages from the queue only when necessary. For that, use {AMQP::Queue#pop}:

queue.pop do |metadata, payload|
  if payload
    puts "Fetched a message: #{payload.inspect}, content_type: #{metadata.content_type}. Shutting down..."
  else
    puts "No messages in the queue"
  end
end

Full example: If the queue is empty, then the `payload` argument will be nil, otherwise arguments are identical to those of the {AMQP::Queue#subscribe} callback. h2. Unsubscribing from messages Sometimes it is necessary to unsubscribe from messages without deleting a queue. To do that, use the {AMQP::Queue#unsubscribe} method:

queue.unsubscribe

By default {AMQP::Queue#unsubscribe} uses the ":noack" option to inform the broker that there is no need to send a confirmation. In other words, it does not expect you to pass in a callback, because the consumer tag on the queue instance and the registered callback for messages are cleared immediately. If an application needs to execute a piece of code after the broker response arrives, {AMQP::Queue#unsubscribe} takes an optional callback:

queue.unsubscribe do |unbind_ok|
  # server response arrived, handle it if necessary...
end

Full example: In AMQP parlance, unsubscribing from messages is often referred to as "cancelling a consumer". Once a consumer is cancelled, messages will no longer be delivered to it, however, due to the asynchronous nature of the protocol, it is possible for "in flight" messages to be received after this call completes. Fetching messages with {AMQP::Queue#pop} is still possible even after a consumer is cancelled. h2. Unbinding queues from exchanges To unbind a queue from an exchange use {AMQP::Queue#unbind}:

queue.unbind(exchange)

Full example: Note that trying to unbind a queue from an exchange that the queue was never bound to will result in a channel-level exception. h2. Querying the number of messages in a queue It is possible to query the number of messages sitting in the queue by declaring the queue with the ":passive" attribute set. The response (`queue.declare-ok` AMQP method) will include the number of messages along with other attributes. However, the amqp gem provides a convenience method, {AMQP::Queue#status}:

queue.status do |number_of_messages, number_of_consumers|
  puts
  puts "# of messages in the queue #{queue.name} = #{number_of_messages}"
  puts
end

Full example: h2. Querying the number of consumers on a queue It is possible to query the number of consumers on a queue by declaring the queue with the ":passive" attribute set. The response (`queue.declare-ok` AMQP method) will include the number of consumers along with other attributes. However, the amqp gem provides a convenience method, {AMQP::Queue#status}:

queue.status do |number_of_messages, number_of_consumers|
  puts
  puts "# of consumers on the queue #{queue.name} = #{number_of_consumers}"
  puts
end

Full example: h2. Purging queues It is possible to purge a queue (remove all of the messages from it) using {AMQP::Queue#purge}:

queue.purge

This method takes an optional callback. However, remember that this operation is performed asynchronously. To run a piece of code when the AMQP broker confirms that a queue has been purged, use a callback that {AMQP::Queue#purge} takes:

queue.purge do |_|
  puts "Purged #{queue.name}"
end

Full example: Note that this example purges a newly declared queue with a unique server-generated name. When a queue is declared, it is empty, so for server-named queues, there is no need to purge them before they are used. h2. Deleting queues To delete a queue, use {AMQP::Queue#delete}. When a queue is deleted, all of the messages in it are deleted as well.

queue.delete

This method takes an optional callback. However, remember that this operation is performed asynchronously. To run a piece of code when the AMQP broker confirms that a queue has been deleted, use a callback that {AMQP::Queue#delete} takes:

queue.delete do |_|
  puts "Deleted #{queue.name}"
end

Full example: h2. Objects as message consumers and unit testing consumers in isolation Since Ruby is a genuine object-oriented language, it is important to demonstrate how the Ruby amqp gem can be integrated into rich object-oriented code. This part of the guide focuses on queues and the problems/solutions concerning consumer applications (applications that primarily receive and process messages, as opposed to producers that publish them). An {AMQP::Queue#subscribe} callback does not have to be a block. It can be any Ruby object that responds to the `call` method. A common technique is to combine {http://rubydoc.info/stdlib/core/1.8.7/Object:method Object#method} and {http://rubydoc.info/stdlib/core/1.8.7/Method:to_proc Method#to_proc} and use object methods as message handlers. An example to demonstrate this technique:

class Consumer

  #
  # API
  #

  def initialize(channel, queue_name = AMQ::Protocol::EMPTY_STRING)
    @queue_name = queue_name

    @channel    = channel
    # Consumer#handle_channel_exception will handle channel
    # exceptions. Keep in mind that you can only register one error handler,
    # so the last one registered "wins".
    @channel.on_error(&method(:handle_channel_exception))
  end # initialize

  def start
    @queue = @channel.queue(@queue_name, :exclusive => true)
    # #handle_message method will be handling messages routed to @queue
    @queue.subscribe(&method(:handle_message))
  end # start



  #
  # Implementation
  #

  def handle_message(metadata, payload)
    puts "Received a message: #{payload}, content_type = #{metadata.content_type}"
  end # handle_message(metadata, payload)

  def handle_channel_exception(channel, channel_close)
    puts "Oops... a channel-level exception: code = #{channel_close.reply_code}, message = #{channel_close.reply_text}"
  end # handle_channel_exception(channel, channel_close)
end

Full example: In this example, `Consumer` instances have to be instantiated with an {AMQP::Channel} instance. If the message handling was done by an aggregated object, it would completely separate the handling logic and would be make it easy to unit test in isolation:

class Consumer

  #
  # API
  #

  def handle_message(metadata, payload)
    puts "Received a message: #{payload}, content_type = #{metadata.content_type}"
  end # handle_message(metadata, payload)
end


class Worker

  #
  # API
  #


  def initialize(channel, queue_name = AMQ::Protocol::EMPTY_STRING, consumer = Consumer.new)
    @queue_name = queue_name

    @channel    = channel
    @channel.on_error(&method(:handle_channel_exception))

    @consumer   = consumer
  end # initialize

  def start
    @queue = @channel.queue(@queue_name, :exclusive => true)
    @queue.subscribe(&@consumer.method(:handle_message))
  end # start


  #
  # Implementation
  #

  def handle_channel_exception(channel, channel_close)
    puts "Oops... a channel-level exception: code = #{channel_close.reply_code}, message = #{channel_close.reply_text}"
  end # handle_channel_exception(channel, channel_close)
end

Full example: Note that the `Consumer` class demonstrated above can be easily tested in isolation without spinning up any AMQP connections:

require "ostruct"
require "json"

# RSpec example
describe Consumer do
  describe "when a new message arrives" do
    subject { described_class.new }

    let(:metadata) do
      o = OpenStruct.new

      o.content_type = "application/json"
      o
    end
    let(:payload)  { JSON.encode({ :command => "reload_config" }) }

    it "does some useful work" do
      # check preconditions here if necessary

      subject.handle_message(metadata, payload)

      # add your code expectations here
    end
  end
end

TBD h2. Queue durability vs message durability See {file:docs/Durability.textile Durability guide} h2. Error handling and recovery See {file:docs/ErrorHandling.textile Error handling and recovery guide} h2. Vendor-specific extensions related to queues See {file:docs/VendorSpecificExtensions.textile Vendor-specific Extensions guide} h2. What to read next The documentation is organized as several {file:docs/DocumentationGuidesIndex.textile documentation guides}, covering all kinds of topics. Guides related to this one are: * {file:docs/Exchanges.textile Working With Exchanges} * {file:docs/Bindings.textile Bindings} * {file:docs/ErrorHandling.textile Error handling and recovery} RabbitMQ implements a number of extensions to AMQP v0.9.1 functionality that are covered in the {file:docs/VendorSpecificExtensions.textile Vendor-specific Extensions guide}. At least one extension, per-queue messages time-to-live (TTL), is related to this guide and can be used with the amqp gem v0.8.0 and later. h2. Authors This guide was written by "Michael Klishin":http://twitter.com/michaelklishin and edited by "Chris Duncan":https://twitter.com/celldee. h2. Tell us what you think! Please take a moment to tell us what you think about this guide "on Twitter":http://twitter.com/rubyamqp or the "Ruby AMQP mailing list":http://groups.google.com/group/ruby-amqp. Let us know what was unclear or what has not been covered. Maybe you do not like the guide style or grammar or discover spelling mistakes. Reader feedback is key to making the documentation better. If, for some reason, you cannot use the communication channels mentioned above, you can "contact the author of the guides directly":mailto:michael@novemberain.com?subject=amqp%20gem%20documentation
amqp-1.8.0/docs/diagrams/0000755000004100000410000000000013321132064015220 5ustar www-datawww-dataamqp-1.8.0/docs/diagrams/007_rabbitmq_publisher_confirms.png0000644000004100000410000027052513321132064024105 0ustar www-datawww-dataPNG  IHDRs2 pHYs   IDATxO<$ P\b-_i)V ))ʫs TjF졟IgV⠲]w5WLmi}MvȳR[ )rXr ,PO~l\3t)?_"[Se"_͡[%QʧL-r Z[Ch0xa 6H˧WM@U :_QF(7Ad t#lVRW޻wUMG?EoqW nfe4ꪴO}U)VWW._?Kew? -Bٝ/Zr{Vf{yvTVs\LN$6 HFK}cpꩧ)oyluCeqWIi=]a*+xBa<*)qh)Z\]Μ*^r[ͺ⨩;0iVqJ( df9'p Puwz^3%+_9 a^X|=tìy>}oY㏯erH*5kq="Vc&p>aK6<>CtJy"M v% }nGCS+,_ v-gn- §KV^yemR%XfOMs9E4N9*.g`e>pX?%Sȣ;8/쮸kg9z4t`hn×bY∥:-9.EU"`y /\f͊+^{Ozu]V4)UW]}衇Dgt 6/Rj-RG}t+obI+~7ΖzJ[ݮc)zbyh'6LpM 7Ж#\UYhQ%HƏo˜mwNkK[gTu0PɞI N ;s۵ ;v_Jp=?F L0Apmt^T]Qڸ྆ pC_Ʈ ",o,_v9ܓ`a+뀍DZ5ڕm\lP Fgrm9,FۊM[8}Iژ@̌H.\ꀨY1;8;6rug 'xbAe%XJ]2^4>1,\9\bFL񱄬,"/̺`,,̒Əg%Joxf [J\_UNjZ"NXZhK4OȕkKԹٶ[Z[S)gtүbq>.%c,f> Y,=,{W%(s. :4+VPvNĜ|鸾7hKju1["4fzb&?;`͹Jvr],s̴ 80SԖg]˟Qkb_?W4`g沖PXJ/J<2n\ =?~&h_xDC$A ԫ\b`?x7*^W=U3xOefMVɀQ`w=M-m!wz)/,i}>Z%w:US.44^,cr>sJ*{0d`{uh{(ͶXiWԙ;2l=_#,̚MCZV2P⥍S\JUJks9-dcN(e͚XQ L KW}a ڵ{u*I 'G9ex/aƻLe-![Jf`쬳Κ2Drհ%xyg/$@mNt-v IK(|ځeJXSv=33e+o_b`nVV=w*Ի`iU5S:RQK ʩ35~;Q뮱XՔ3vӆqbdFU3r`wl6rLiͰQ7Iϳ,͸d}iaV窨\P.LMvVnN9[Y2w2*Tҿ_~-'aY7X-XFM<ŲJo:S^?p,m/Su@U-e$| 1# Y,w i~&h{T.۞1s @vHgTDw9,d~~S9昊r?˩zUuOљTI="GEIԝhA.I'>F&௛`N){f %G3:n%MJǻXB[KU K̮>PB;y)RrY}\>TrgUY V`[7LGӫP\i[o-wM?,Xƃ2 /;,Yně`6ze`4H[S:R[xRw p<fA?>䩧Z/屬 bXfW;Z[C$>]tSAv;=L{T6q@ 4TC('g0q;X R="Tg՜ (lI$P@>&MO M ^Mv|h(C=TQDx )A D4@-*)f2;֘)>+6ğ}L~K$x7I, 6~[M>|ٮf_w lcm8q$/e~"A S>f5-aGa,qmU->D#7+mx5]>Q̢WJkUܗT.x6:bcm2V˥?plvh'QEҗH*ЏUM8};q1KvuYꪫ1}6jHN7| n=XtZx3&үdv,vnT)rdŕEc+>dff0&~O`PRg %מ6{套^r/Fz8oʥ^n+kڣ-U+*;c%07#whFà r3hX9p5GjȔH`' }kvbHfUWlc$(*x_ѻlVTr2)?G,|3(V&!^z'Twzi3,~ ZxcÙ=أoa`-qcr6aÆ.r89%Cx$(yϼ`6%;b-{NI~ڳu]7&Ta Z[RH( +vƭ͖G[˺b 4h1(?6gC_|b=à[6ﮌx$A4L#+ň$ @%Zvv`R+ttؔc痵uLA췐:th-\w6-)UH[gAu?;=yH˃3.˪Kwʨ2gK*iu]Y\T/N9T))ÀTӯF9昂Z.:!o*2jt~״bTm5U x`*W1 o~GkmMw_71>[/tә|-³,i냤llxAgSlY窐T,VkFS?RPríؗMY|HM胡%\R6x׽Jro롛2lު2Lסzi:{"C4eM^".Iڇ/$ LaLfv0;[f*mJ4k/['_K(1S|&&n̄bf=;'>.yZfk=-|3y̚`V멧|vы IDAT*i` w&,oύ:7CJvD1]aNSp̢aFIqx5w{jege(fwv`&ul{}S:x5»gu}.G^vhᄏT6 Jcbƶ* =mt~65f$0uSWX F` ,qߍΏIKͲK^utx7,e 찋(>+)8kNRx I|a^JiKDƁ]G+kGi/."6{[xS(:=8|,$ XnUA|\[oR_,ƧUjQ^mZ{\#,%N(>3`./Wm)(>s%J^Vh=ʇyaK,-5]|"}!Ԛ&#БEc5b, P1kH$@U }2HS)DZN)IHH:g G`   h@/4: t T.;ef%IHHHHHm Pl[LHHHHH:*2$@$@$@$@$@$жOe$@$)~2f+HC$@$@$S$$@$@$@$@$@$;.)$     Ge1 NeH t>T.;5gIHHHHH wT.sGIHHHHHr9kL$@$@$@$@$@r;R&H$@$@$@$@$@wYc     ȝܑ2A     |\vk @\掔 @#@]s֘HHHHHr'@2wLHHHHH:*$@$@$@$@$@$;*#e$@$@$@$@$@$P|ל5&     P)$     Ge1 NeH t>T.;5gIHHHHH wT.sGIHHHHHr9kL$@$@$@$@$@r;R&H$@$@$@$@$@wYc     ȝܑ2A     |\vk @\掔 @#@]s֘HHHHHr'@2wLHHHHH:*$@$@$@$@$@$;*#e$@$@$@$@$@$P|ל5&     P)$     Ge1 NeH t>T.;5gIHHHHH wT.sGIHHHHHr9kL$@$@$@$@$@r;R&H$@$@$@$@$@wYc     ȝܑ2A     |w*$@$@$@$@$1 _V$P@.]JB%MPl @48i$Fi@#d׮avw#q\9*Dž:lgΞw# L0/J0W4ef&[pe9" ?vfǡm\f;w<1r~q8$05;F='B$@$@u]8qe2Ӧ æ!f `,̟~I+{6nݺY.4W2ށ%؝φqGuw(vǝe t>J/"8N9ᅲ a='3駟ޔc=Vva޽{xBt32V ]ޡpa(Džnmu'H :xn{|$@$@$@{1G)zw߽rk%|;ܪ_ ܃SΊ@h?o,Ē裏ʅ^(=ztT qhiBWQtށF)7xvO?rRJ3<9眲˶n+;)P2iKpӐ tl>F d2tP/Yf3-A'F1~vhD~-Z5,b2zOKC7f1%eiի~u4J; 7Φ9Y{~s9Gnà :{ ?~p@8I$@$@$ `lcryUWɐYfQ !ad6o"ag(ȯӧO9믷w1v؍9hD4Նg;ljd%38Cfuֆ`$L0;?,|Ozi3x/L\XW  `1?, 6Yy噧ddeƯSx=bO>]N~ifil^z2Y,Ū9_9nx沒b2A4h ja$P$C>#[n=?M9Xt t$'+=3ffe?q2f0طB@WnFe`c"jZi?*ۨ? &zLCxfD. w-ov=, @駟. /2p@[اޥ ,xJ$@$@$І|ƗǦʥ[cnJiQIjORX8㗲I _G]X8if.of{gKak# Ї[n%q^ ЛHHH `>2V"TIm_5J)]YO?(KFG#Bګ5f͆K; : cvm֏ l2T,Gߊ+/ c Ja2f\p%s NLQҙر<_J NcL@qfCʥg ?ܖY `9,VޥX58# x fKݕJEbSmʓ)~$Dg.Q.~3qҟ%vc9:A$8g][ݻw$޽= HHHc2l> Kl/m⻿L1~᜜ɯ(s~xmF\f.Y\bА 4O} } ?Q? @$ʥ+(eo'E16{[Xd@8CS1wWh?*v۪ێK$ a\År  @C|+n&F$@$@.*рߝ8Mr0s6v188Ov-5PlJ!?)3+~B+o3^vmȟ/ρ) D'ba۹CiJWHUΦg.(     NETn?63$=ی_d3!ۏf.s(     _Ίں/Bና˸ʖKSPxq;K_v>Eb*$@x?s HHHrr_= 3pgb/viH֏4&| d25   !ڑ"U0qs(Uf!]4~֔eyd:$@$@$@$@$P S]KTѕPeyGd,2|3ȼO/m9ڋJ>eھd>=4=d2f8;F/??w<7۟ 5ڲK׮ n3m   z@a5v~!?'h@h'Zʵbh..*`lDƍ'>\{{de!u Yrg-ڌN~ݥ_>h1BaG3A\+}N +&   ȓ@d46 " (dYsT.&k9rEȀf6_X9>ccnsnސ\LDkrׯbv@ɝyU~Sm9cd]w={I]( t>>r٤čAMOl "?ơbudB裏dW.0#r={u.;レ̔(]T!Vv1nKr]GxPN Mt$=wreyO?M' @LQLO}sOҧ?(ێ6en* 3ژ1ccuBw'BwTKDڵ*z$J+C g&Ԡ& +j/@v^U\h 2Deny`Z$@$@$@$0HR>˚Ȼ˺\ 'nt 9-d& 0 o(nBxHgܥ%"yb7:?-3Q7Wng/e-6ON8$-.ǠM$@$@$@$P@fg"i[,q"y*3b"$EN&SLuO>,"2\c524mUT :Yf5*vzYH/lw8ˉKdb&36{hτZp\,fuck"~i땦 @D@GP }CWMa.:e!' NDr9sZ_O,΢I !K(S*$D3\є#ҙ4$8pvIrNwQӰtӟh>iD30\ye]R\ulI^HHHH*H_.68c; gQ v?aɧʣO<%_yU&cM+Qӣ{wYlEdEN2Ẕ\\uBOW39ܤId}6Dxx?MXڌ*Db2r`6TK-\vf8mhL!ψmd3Ex "_̈{ZO}s]wA4$@$@$@$@@YfC suFywncF|o̳˘c5B<׋-.\D^1x-ڈz>!r277n~7rk.L[HU`{Zf؅fS|k\60WxhL l-DL:R'}#/>:&HHHH Kc1O0̈́Ⴗ1$- s/ 쳏wts92h ;V\ieYg-a,XtWxJfRdr;KoS,P[ g2^";z3dv1#+i<ycF3}7rjC1bX 3Mnk/ X]HHHH W"a[eb$Vv_F.]|f+SV99JI[^뜮Ηe@~5n\yŵrK=Ҧm!,-*a&3l9KC‡ËUB AkɠLv2]9UqCZ쪧W'j{u^GN9o*KC$@$@$@$В@B11 ~鉺0KM&GIQkuq*M+xK"?>K\{o#~݆~ěwpYl +ʸ[,ݠl`P(mX;*W܍$ey*gDLI )\iDӆd@ `_,J'd:4N(娷3j?Ę3`?WrU;n,/3<!   ZbRhcQlU.ɹrp6Ftn~V0E#O>DvcW9tO)gKDL)L`;,&f2Ue3y(# 瘍O?PB@Xtx_6aeSF?(Og>"!.#Mc`=_UvK4$@$@$@$@J؆;>/ 3p2Gyhj#_GswrTj?ժ&)*5aMhĉHʲ/& ^_ IDATUFj+&>8"H4ADv`Cۘ'o^pw?}#SfB9Z^u@VvSi4))z~Jc>4$@$@$@$@ H2L&oـhk|/Z\L.9Z!mbM r8tU>DG@Sl)lP2])r+r _/teF9|ʧ+o $$R }2 1YiKS:Cb!O?r}7L F Q*&m Ed[yp5[ݧ愒Sr]8ׯ\9_ s?\~r{ixج)L #2(soI;,B계P]bm:]BHGX$h\ёlԣAx2|DO=KְK-F>(eT^R?.>u^xXV$ۄA^)w\܎&   C R20!MVK #qPGRw[/) O*kᮟˆ{T&`dF+8Ѓd]ldcm.]IjU,U x %ā<d*ʠuA2ZK?iLKLi:Цy$C-t+AޞZ4gтʅ[/:h˂_IJ˹DJ$@$@$@Hр~Adv4 QoOpAkjr_J^$q4I੧ǟxD?hп2XTLTu+Bs%JrW(hvB'~Д3J+ fcHHHA&nI>;K)~\[Huer0b?Z<|MF?;bꐥ3,^PԅEțs[ú奖PTMYtR0Q0f?V*m&2C+֕W,r ovf_MLBE/$^lvY}ryD!;cn(P+>%! fQHHHڈ@%'+d!n2 2yO*>ytN OAIÇo!nt/CZ4x6cJXV6 'Oz:m/G yxX: HyfPj9A PW"76ҷK-[neI9wϞDMvXBYte7l|ǧ_W>! fMQHHHڜĆvlS  "f\D!\rf}%cNe)EVi!5W҆j 'HiSĹ)xj o-C$>3!kPq%]/Z&i^YrgoaÆ*4pDU7N7<B=ZP E9516^~1 IixV3 Q| v* (NrwpS*? "r~q8ʆzdِeuwjbޥOrug֗6@8$vngrcSl3<#,6o[ nF3~=}uÙ* AtHz{҆5aS(DrLgETT%#Vޱf]5(4ʄJ!-OAWD68Ǝ - [vyeKرcwޭ(b9[eɅa^]e{[wDǣV>qS+ky&ir!B6k| <}+#ljYWrnE!k=ϐM#]k" Ku hyILZH+~b$)X"p'+Ml/Q 42lAʊ*6H z C^ ID$~7^S g Cp nUmG y&3a0҄IPBa  ұTvRO,ri9_ \+#~B~;KS^觞ҲN# Ҩ9>)C ]r0?w& ?\_Tj G(("FSbd 04iI.jr'xBV\uD b'@LINHUOhCfI\ g" ).K{xODq;E=;ĆyLL@ rSt1ϧS]|nyeUW5܌-|vl87i_s[E'»-+CrE90 %HNe 2<rc ߺ մsX8zbp `Q= a;gqVc|̘ܵKldỲ3|/ oS@I hѥP|77W\ݮ{ ~ `W.vԞ"I= 𪑯6&!`hkYl^9* |䠙,&fӠ8vnڪаL C; 7`h$cb(HjFIØ ʠO KΚ`N餎6f,YJXdu38D~PZa`zb DGh?hݺaChԣQh_?Wz[HzD2SGv9uXe|OFB??=q?kAN3}dC֗CO+A#M0'?oNTEg,]V4;S)N;WsN9蠃~?XXhؠ.M 4gp'gL9e{C㑍n?#$ڇqYP,n<]p 8%4kNa/&_~BoDP"3/(XXEL3%Q0!fZIZH׊]HS` Nn~㡆]v'mnBѿI5i9'z{RoS "&eС2uvK9JtGL;l. LItBR)˂sId~7ip Oԋ׏ΊC֟>Vv6Uw`'UC|DZHb_Y%ʆ{%oI>6#~٫Y0ߟ k22M]msa)b! 4Ho>S2ˈO^#k0/zi$ZrOsmڑN$?ڒk? Dhd&-Cij~|PQxKWuBPf4"d2Ba 5l! Z\ǫl# ~AI| &?XB6sr Y>x}cx1ϰ1Xݯ%e9ʪ=4Ó$/!_;s٨7r{o({ڌBԍ.ilZp@:|r.+kX6VҎ ^~0vtҾ-u/yU.qEkWgySytK_^ɷg?/p~]lk"ЋNa:j?_^+#˯+QK9Z~r\sP"f=ױkUk?jٻ !gʓ"î>KoIe/9^xqyPmMۃ ._?BM&Z8K4'b; IDATȺ/=yvZXvZOI'8m-?4SZ9ffѭh9жϮcK=Q*7=03R w-ffvA}Ql[oVXA^{5e7駟^^zid4ٻK_~yE7~\z~*v 2?SYodf-R^y=z2sZk%=[N;y'kY)~esY؋?. t*K-,0 rc^BM9C4hMy㍷,?yIb.+-z?.;ʝw< ,jrEXO+o+[gw{d=OԺc7r]rUC.8t BQf<ջZ>Ѳeznvnp 2&le00L~Uù#iaۮzxFBZtͯ -B /% S="diHW!{+@Y\y,GQvCrR5AkBkl , kVߖGn?XZ?p~f}(@A'6MGjjXq o*qqޫtzL+=) 6WeG(ӯ5w]#xj^}LY_-_?=ti=G/~E^JoUƁ{[pdKOiʏ~=_{dvM  G;I޸AT6O$}g.kU(!ݿo` _c=Vvm7od%XB>#9d/B[SJ~{93S9ҔR^xaS*\pA`쳏|7믿1f;L=#M?q7qۖpF`W|+G~,NF5# hP"o=`}>hPrꫯ*#F-k~MNnEY[nݻɺl l;YrEGOzO>-tVZIEaZ93S!?[q9s6.'Ɍ3M'[o.%xZP;TJsxjj0, 4=d1g+{<5Z6YPK}|-YM 6KOݐ0hI(\%4BPhpj:+CVMl-!Lsou_/},榲'K7}Iן/}ye_?wE8'PlKlcM^:ayTfYr_&o=z}+Gn(s?s2MeMwztag44D^y 4nwYcϣenJX߸&ѻ,Vg,̫z68lNo|Pe_x,&3I˴3Ϊq k;%w.(C/9L=Ej|ƋFG'/+ם7osHϻ }7lx92Ks7^ߋ%R>}ykec/zs|#p 2TT*]2wjߞ$^cc]Z2/1O_}.kt\->37UYǜZhxޒ@+ȝzW^Kn򐲜q5 ᾣrO^q^-M@fa+㗟!#UE\V`VԐ?Y~=->,CyidMv^|\6k܍,emNYE@`|̤ \fTWEw!V_vrbwYuCLy換jE|mcYgX>!uT 7G;|Gk#-}n~%ۮjy5/?-Ok{e#Ҿ/B~^rr}چgnD6:jpZlh;B&/.ST&2+ |u1NN1l ;TzNpjR:(zC9fSO=%n:_n }6M,9M)4=}O En{Oo>YdEK.{G]tQo&߿dk44ӹyM"nH8?M\QҍS&lUl Kzn+֙;!)gAN> 93嘣Δ矠3}o^U6hYxsN5tY|d7|[ 3$^w\.&3&md=+nld͵W}!]r }BnZjTMIfB4i"f.{Șce?OMy1c70{RSƱ6d?@wO6Q%#OBwx*3pͷ=+f:8kvmlJ=*/?K?U 偳|;]yUw;}ll ^{2~(W>oB>nݫQrw|tx)z^tg<~IX㗟or~8We`t])˩ziҁ5/Fʏ_~b|b~C6zAf÷@sfeشw7-\'HuW%1\2=+}M'ݵ/} _j({0?{`EPTEP@ 0TggSpgxջB G^kdUV7Dž= ֝^'+Y;sigcpMsΖ_ڵ[F0D dp[B\y13 F3hDŽ/},_vqSWh)FAX҈̥BH4oDfWy~l׳taiyAUY:́uj橆nq~J'ؔG,geT ,+JD?L4?xeBf~ā&ne.c^GꃷN=HSN)\Y?(3_:ekY9Ӧ*/,pFc[x:^ᰣr&uY\BQ R}Y|'l~vs.WKE]? 3s6c[Oh~3A:85We!ƮA^@Y{`Fnn 6_!\/+yc:Ogs9`}<.e amMDz F2go0'`fByf4fzv8Pag50|C97MaWޘAҹ&Ы) 0~[f'ovF9v>z>9d_~Jg7?ΞݩWh|ΫM~myV]$SDV^M(|0Ng_+'m:vr}5FI%Z@[˚w26y3,8$`w64eۓ8Pگ W-6ޥۭ) nMl'/?d_[>O!1ay=̈vrVfv 4pv-fd: ̶z惎>˔n0˕2 / 6'\"]hփ7kGo^k9dN_r< LJr͜ *g:JGFT}XUq4ʛԺ>}V~}駺dvܸqNF⇳Ow\Kai80mhdr[)S`a3;ڵ 9 >+rex׀|*3@Ȧt7H3/ȴ3I {2>3ydJ5V2x4nZ /],*ίLuiZ~ Ʌn PyN?Hv}3lȳPfclM7g$ KuVlI1, %w0siEQvZ;3y4 T.5GPhs`u67L;o,ȻƑWq /D ශ,ie9iB|]c/ofn?$ b ^ة|D]w sZԿv{UhDѐľ#P: 6 AM7LxΘ) ìK㻬.<|;A^Bݗ;`דn1l(sChlYM=^>Ò0#9e;fF;Fio<@"uc&0^tGtU ɇ]k4ݤИ0}Xx@V|`"1oY/\ o?&{G@ۭ} ͡T؁'+ˣ|߅O-є>ɶ8UΓ7.{_~׿G~mծiMT~[nrȑ#oQ#rHYQ.2@vUD`UVgyF'Q/,C$*ҽ{w{KnSO\umA巙9 >p0/1wѸ"4.g͜1YfKϔf E82u4'l-dάyr2$_9LΔW^J+/^~++3d={AIG4;f#O_vJ^~U~ltjO n8xx7?ګd!ClƐQ"Gz1>%z޸X~ǀ%ûvCFѨD@3IOcs՗mx@d$sxʶq eX/H"?TcĶ)~!+c\ 3=C&= 1xQ"sgLa" , L\:Qr,.l1h_`-Ch?l+\JǙS&c`.ex{o1UGR%baY'] Xy89Mg1 Zu&5m9{0K(i> SlihtU>x2^Pyl~ou?pԇ3_MWWe3Iށ1D\d O.Mhb8,崞Ak=sáT 73 A7H=&[kK=({9]-S, #˥t\JA?7]3Ugr+D3S bX3{$Uw=Qۃe{趘M[y^4Ha ,,1#ֈˌZXBϝ9MwaA&{\|?)gQIgv,ia. o${WfMEs^=U~^?U>i>?QV3+Xc+x}5L#aC&MXn+f.{^l~&eV*ˋePIqc=nT[gY\玾-,i@ؿGo(W1;/~9#L/n7kP/<Ǥ%_艥__#Vij\\7{%q̞={СCH#FHT?cƌnݺIΝeIy'{YJCz5`/R5.O>d߿?^{+vcd)#9Gڨe~hC*и R;m2\ fZJ8Cۓ6y;E IDAT68NzvEVﺃ躝'/].-"G݈.'vSs&z]X%m і ?,v!L|k%3Q?Pk؎lN_A0eϝ p7.@tf0&PE1}ٕa|$߯mb .?bToO\e?5^~30TMlbr>CPcV oS.vV,_׷ fX3}5}y#i|A,IGY_P'.=Ef4C8Q} a~ :I9f:9p'X~<0T_>i>:ΒOxY}7&0,g?x N9 1r?O|1O|y{}|@sשUmcxHz7Ozٕj;f'}+cs;7KȠ_{QR~荇m\W,'##el5VN(L ~晍3UıM62kKww~Q[} 襎 o2io~A\C|  i_}=gv_quNP27 y,_KS][l1L&Ow'r9w~|:P4ir AypQ-Z{J{@Jj+A@>6%I)&O@ ^l|N 76CMnW80ȟZu̇A!ՏuRGcѐ:|O9k%Sa֢m;IWqG#/b12cv0A Sscat-wuee?uKwŌ"lGyk䲝LfAY}YÜnj& NAGLy tv3\Hᶫ*is0gv2 CC;Ż7_Lbv6(Li1Z(?.׼쓠Ac#O㗔dцw:r1V^Rإn6,:PrsUYA< {'͚nG+fNYɐrY 9 9GrC,gi_D )C?^n9nwpe=!x™^o!_f^aX|\*?bǰxy3`O3*A;Ĭ&+ 71땎61br-qbVʉ%j(IL p;o,Ir]s> 1+צ Z?UJ5˟Yieyi߸>"eȔeQWԶ(-Y\>eyԨ|ls?sd )faV 3} nPK1}7^YδtԌđ؜х3iYg{ϣgZ>{ vuW^s`H6gPmWML ]+}WHXK>~֧ǏXOpC&ʡ;}r6Ysxy>zO'x $3Yi~w;}-Y\4_oyQ經&?[t:&lYu9W:`~ײetֺ+34x]rR˞__5ttGGӫhizt6 QYIW*k;t}FL"+(kDLWB^h]^"cs44հ &]B秼AGC3f0Z+t^^nc<IaT$fK,$}Ɔoi <\4$X8c1o"=V4ܾgd}fV>EyrgO;!ai06WH@"?Q$ q nH2mCbabwMlzrđyrg怲3{r](r_"'\60~oMq20*%.x +&PsM9ZsL3g_9"gl?t]1`~4U5\)y-Xq'"n'rMΕTUo+wI(xzR":@fݼeWB2*J=1~p2Krc\ & <0 yL0y eҕrT 0}7sm!VXΣ"yu] 1%?U~F \3\|8F{_k<c-POi~4ު;ϟx?/Ta ԿWXb)?C G(7F{ga̕ a]].E5\szr{{3o{y$ygg邏 (#e[[vA:ऍ[O ޵XJݴN'i6kz\}tZuill[I2=ndy !ۑ|ŷG=U~*#>Wį25Ϳ>oW)\zٹpu( V&x˿0_(<ԥP?OmIW^`\v§Sjxf]şp3b0_\v”Dޯq;Kb1_/[d>7x9a\ &+G$O9YL"w/SE-,dP_Qe*?"I9_AE HGyeNt]ȧ)"Z)6C*|GgykoHN37vyymg[609ژa@Wo0;%ßRh\V7X?f4 y?iË}BrUw]!}Պ4mgF0$nLD.Dwt\n.x*bW{:xÍ7Z.XT0 4T-l@u`'`>ir#9~# '`a!k+zϐc>!o'<>@ 4G-JJ0e'\~0ƧIY,K?Wۏ~B :߯ ^ 'oM).饂뎾$qǵ*r$e*5!kq}prA0|<+;”fWU '-{Mkג:hL 1 )ƯR' L)@}R=rhtxOît <**Ʊʯ(WdV8oJ%C =AI-O KN8䠻RђG#=:Ipxr8LJ2mHUG B8!y-) =? | 4o˂5@Ee)?u 1:*6qQCggtcUrؗZU%.6S-+.<ـj?7@4.nF?ѲU ^I[vyh IeĶ<ڸ6$>TeӔNpBtzC Zg5pGg}YB Ay*,g?Sv\oFW+7|}ڋ5]y5u;W9,X>LBj=k L &/Vit o']LS_ guiĜY.EҔ6al8ԕ)V> +]#Nԗ4Es)C]*nUB= THOY)UA( g>[a^sPtRSZGGDy윴Z~FCV /%sy[ej&yUDmeњ6# uo:5tw eT}IYLǴ.X gM0g̘!gNǞ.gy=Z/ii"L¨^Mp4ƻƗ|7^D3Cj"f) Xbpv8SI akSճh\V174~khSs~Q0-4mC#jر}G0*!GK:ŽNGf#=ka'm4Dm)vTET4zpI$= tCT8K[LaC;Bg6^c0y'?dйBn~00&WhI$gBZZY<,qtbr OҘ4 UB:veuSNQ4X*}tYy%tح :ybOS? *_'hjr zQy ʢN=i<1\tCBL&)^68/ռ!K}Бi+D'ȵb:e:~7e"V@CD{JDfWRڐyry2-X2> `LNȿh!0R>Ze̫4^{u%[]hlY ),AA͵LO'Gp.xߒ40=?9R ?]$w;iZ0ЌE@♿[(WRp,:Da h3g.7_V e< gџYwŶ?KV2QkB>;B:'TQJv>YgmZ-[;/JԿ:yE(5.4iH< E渲 eWjRCW#w* k#E&yׄwQx|V++t_E~uU"}<=+YqNuA1g-yO8dqiwO[ '48 ik:=.c)&_`cćrت~"&(qJTI 7UgNKCYyѸ&F![{fxzFj IDAT\2֤]Юa.;S,19!?O#a!vHHb<9<6lfD% Ͱ仢4:h D7<Uɪpsg?Ad7rQ[. 2hE|[0eP7bȣ Ԭ;yиtB~vߒ⬹wȿ-Ŏލ,HUU9U/OȪ oy:yZe/iWy+|VMeՔN'kytJ\]ӧP_^n\ri<熅<罛es>9||f6aP7ܘ/{i`^>/2^䨏i6f%U>~,qH(iX Wa"։ן6?E㲚@Wڶi'o9QO M].-̞X+GCӾWiJ1l3S߽L N唧$SRHޕQF=ѫyf0'l 뭴LNB'i\el S, W^ K^.u.OVʭ._yTfMGC[S.{Щ6X3IE\7CT"jk#(7^ԑY^W%G|%BBݼ=]+40WyYIS΍˴ƥwgqk_#ky)C1G]С6Yfj346Y囕nw[~E(W^װLа%<蚅w$ßQ`瘉NOii0pPP8OrZjdE(^˩gm)lo Nc.#cљH ,ax쥅B34J5XM;OfvyLg؝?sx5`\ZϤp\d3"325{2|pd?mIvfhH=tV2ddFiBarYA$\DF_8BˋtKU@ko& %HI:fʪgl1ꆪ#ne/ʉ'OTIx'60!N X<\?QW!tjYGpq2\~]Ƥd/)jK'+뛾{i*KyLڒbI_HSKTBW߰QI糖c1N8Mo:HE9a`SMgX8C~3D\zpaUC!?kI}x%aDmCsT0A\NLBѸ!zfs{ uߛ孼0yʥ>>;6CMGw9MR6(5E ]9IZ,gc˨.H11t lT)IUN!0H{J?Π;gh\˚kzpdKb9hDh0Tc 0LY,;ȇ߹S~*WR~̡دi7oƪAId$f#2X6Io|}1 %:v!;inݣBy-eΥ48#O[ 92w:b7P,uegYK[]ʻߪZ>όA p:afBCBYC^by~ѯab) NR  SwfG29B}Ԏv B~ "V/CFD2,e\BG2_Brq.30 [el 'n,^4@9l7 q<5M-g=RMhdQ̀otc=<56ΈR'δ.UndڬQ0.}ntmr\ձ8rA|/2$Bɡ,H;}tGgiTR.7 j|: dm?.GB殻ɠ ʀAf,%.y0JuSy/$;'W]S;]N@ 4͙O7}eUiɅC#Љjc몗`ps=?ȫGP`F#%rIlڥ I."D"@@i:x*ASm8/~~["7:ubu],T%EQLq+\Mri@CS  @Q43n(/mU/q*G Ax̣0Z_3l4 :-$_ꊐ.eƓD6[_ ?=BKXkh0ҹHrJi_OOD "D"%>1_"Ct|2#`?/ɵW_V(S7NX. O((gPFUIJv-u̞5;H2h2g!>18|k|K RCiADW5]Joejej!r^6it?a!7x*ƹ3ӀL0s&fID"@DpKGCSqiOe>=K/(ͣ OZHiyːV%3";̓tE֓#KE3xSc,ϐ-/j 23m`ov-ȏfSWp֒3. P+=uuCEF~\xŲ6۰h6.ӳN3U" D"@D "Ph|йogcyd˿A2j{0=SOg)?rWIYD4.k_7p4kY|jiE6pܔ\b̑na I|8̀3̟R@جǛ聆!9̅2i2d_C_.0-.=܈@D "D"UB ا DXA\8YX.!g/>eey_ z|i҄ &>dS`F>4f;=CfC[A!UvbW8!J!IH:m032^(?ib4<޹4*UF>~"{x{z*6YhǼ"@D "6:Kmd2XNy: Y!:Je_;͛5Y=+Wr\z;ȴcwyZV>=ʇb%qhɮqE%|<^vy{y\uҢes4 9;l_|6s Hb ip !! PNKCbWs4rͰ$Vu3>Kao;fAE"@D "$9W.1:c3|0?⏻ |G9L韄CZ>CR"$eF_SP58bk^eY?,{n(K/oU9aӜ".I tsj}Y0&y^6#+Qi,byIDî{p\pr)i\D"@D "X4@x`da,LsG'G.n*l?o=xk?9LBѸƊ^{㾲&3KJܘ s ,9.tqE_l3w(C/۲rh2i8r&ӝ74)3mޓVZYD"@D "P:n,qz|Ao Ep 7md"0 6H>xcț}́0U5L3afQg 0~73hw1Goeu*;D"@D "PT?4 (͈t6I Q7B`z):RDBb6ˌRLϕSOc{+y㕉K_ˏ?L=F!wq 4ɣD"@D "Py>j SQho8QH\O™I x4kLf=fϞ-O<vg}V.:ҥk;}ֽy&ҪeSiѲ&2c\5s̞5WfΘ3_ME7٧ˠM71M6DM"@D "Dj ZLXR# K L72]o#ni?ގ2q[LlN;M&o|2~'}ĉoȌ3e̙0$gɜ9UҪuKiݪiFzX]z\Ov޾S(M4YG@D "D"iAmI'0 ZrլTh\fѶm[lX,#@D "DnE9$0%"/5qnh+44?\{LQ~̠"@D "D"AFIvƩ?fzs?pʷC,8sQPD "D"@D$aR^"!/@Yv~a fH#dvc<,P2"@D "D"UE@#)͟t5'<"?sh{yke @D "D"#a _g p2liF[N oUDDE xѸ\$41!"D"@D "3czU4_J|IT6h\fcD"@D "$t+6bU6.GRm 'Th'QPD "D"@D2YG~j.m F[  mZVfq23(@D "D"@%Hf-iHf!=}RA-Mk"~"bt;n+D)I4y7l TӬ~qQND "D"@F mPz}QY,r ʁEh'D%ZL@嚛fw&h\fcD"@D "Pp#~Ƥ8Whg|ƑIe IDAT 90B"ZGfp~LJfѸ (("D"@D>#%%%R2ciib5lذfF4KEl$#?W4 (pհ$ .@'@D "D"z@Q Òs9@|̟?_.\(48{W⻁$tƗaӅOD|F֑$e68F)@D "D"7黱4i*21Y p35Ra:d`H'R/{-Ox#!mTnNe F@D "D"`AS5;a`~33Gj~2lq1Q\D "D"@B K>c0Gu JKo7y%dg` FaRcF%jrxuߑHf.#~'2@\ QJD ",Ep]D "@;Ӏd m:آ) a2ju&M`JӦX> ǝdXՈb?0\Pș'n DA3<Vh\&U_ ,j ut@A ^K.j{_KaiX,:d|DJ5Lyv_{<`h`Ҹp,g a`s>"ܠȐ0 "~ ۏgG2# @o ϗ獏7*m"Kt=,EŋuPg&*X$= K.surܹj˗:[ Rm&22~y84L~XFm08 q6U)]U psL"~!yngE2tѸ(*"~JߐndҰt,_\2ꃎ5-5Ũ>]W:e]KK[UC[7,J5; 66 yY)}D]dQR /w/i4qM.i(<+nY@mĂˤq f^@F_,rOo OqYC#{D k&>STy|ӀEuf_U}Ju]E̗\Gs/}XAzpyVa@lQ9!uiq9"y& i8QH/I%|^^n 87@\I6~KJy-Df5xA#\cvK:R~)}s$Wop~#TJ{ayz.r|m>Mn&kyqhRrr:#&|+A/8"HcQ[ [G%C|Х,mHWs8n']KMO1kq'Zt:?RWe}Cc\Ot 9a&{!xD5]l٥[$(gJHEbP&Mռ@g*!0'I $2ՌĹpBcN~:" i$,,/dqJI7Y΃0-E+ @ i{ZZ絘P^/QNW^esNbRRp~MRtt""30♍i!>iv%jf"t$ a'؅:-L>+U:i6Lx^xzpǞ%3B3lW& Jp#( ^y4!2Mڏh\fd)@ KޔVLZC 0F#@3fCE~A~x1@agP[Hn22n1p~{uQTWQW?403bp,`4á#=5QÖq BZO4E2…"Rqa&f0)}9)yLֱϒqi-}ʑF0;&?ŠZ)cr"E~:_ 'h\fcйgFLtI'ޔw>Ye<"tȯ#~ \:P:p>-:r^j~p#z;O_"U[ yY)hLZuT2}B1 D7we<~.A?n+v ;]4.34T"xa " H@é~ݧ. <hZ_?:}{4= )>[ \sƗ?ěH8NEeh`>,l|κwt%|ߩn\ŚxGi ,$t|-Dۏ~qQPDo@:syPǎ1?n~7?IH~^?%lב? u<5a;'M.fP3 >#~l(lbI`(iDsf0_U3\3 J,!\2…p@sthBE~"~o?ڠ %E"5CTnd4+0Jn5~fDG`>v;9~laL~]3[58t99 eʯlV`),#YJSt6 Sي_?>W "?i5ˆm];g~Я~_myFMՇ5n<?^p\DygTFބ)tgߧam,4'?Wfe`FQ @c΍J>M( ^$=}ax-Q8]_RI 1R}ĝrmo&fX6GGEx嵯d /o)kS&'> _ahk6hqE;6BU׽H>lگVkjzAj/&އ.xCJ-se5ힷd5^y+9㢧XYo21wm?H-62AzO}~!bZ-3o:?ϋoSbm8ҩ9rI\,eaqmKI/֐K;foلs00erÜi O"CwZVp3i?"yė4FyŒY";dI/_゚*[/Yי~7˻~ot}dЖ\U7gY* ޢ8~2g|o=P݂.+{)+>A6*}]2mڜD+~}˵o%#닣ܾ@tˡ=eϝ?b0#Ryɏ|> 7&knr3p4M~SS$z}rpY9ܠ4Gߝvv $v}oU2kCX.T}Q[( f-K?C4.9sC~R}V3P!s-tcr7E^x%S`2e&0F!Ń@noݠ0F0LP 10~~[_cXn(yrAr/K[O- GT^/M`ixN>GrGg?:L9a 9spΣr678Jp+\"`('>Tlޅs*5m(sݧջ/'{O+c^N?f39e6 E1?A0|ʑ +o~Bp~iZRoz u4_ zk Y#6ﭸ \ Zskbu_\>/1.sI}ZG#Tr=5%22FҺo:KܩڇAr)YK2ąyN0" _|Ͱi?9LBqYl&0F!#9ԛ2.\#S#W*WGn-nb>zC,Цfd>+JMd^+'0 g d3d2M6XU^y0iۺ2e[zxئ0:ݖ̛.fϞ'W]3dtysKnqGŨ'o%[èz뭁uV׺˛ev[,.Սngٿl6{[PXbYTSMlxw|æ.8NJeZ8^u9tS=NXtW|kL5Y+SbKʹ0ZS7(vO[5o*E[b]?b4 ;G5Xi3RMO7,7P-m,j\;~=ƏG{b'ѭ8ea4|!A!ZqSߪ)uK-:G8̻G,!E\eY& LmGW=oRZ:bƒ!oݑh}ic145tF l=yWfj_ ]sMľ}0x IN?ݗ3w&' g's#7/"Wsud$I\g ~Ʃ!Guz e8UsiXۋ/Eljh8۞jYxaur͞?J0uEb Pg_<:zf g^. P]t̘_> !W /8\v^D‡c4$gQ9dU5BC쇑* .  ?a 9&&wf#>v6އ2xx hLt "}sT˹m}fY]uQ7 龳΄&tMD;,2|U rESuOOY\G\9܂ƕb |V ]Sʸ~=F`[1Qb`L]HJ{!Gg]C=)> BXy:P&o ?Ab7Ύ8e U΍SYuhx3q*W ^nP\Sx\bk7`]k4&qg3#q0_G IDAT=7L Njqkcx׌Hju[&k%$/b:ǬvC{V܇A; B+z~")bz"u{y B |ǗgwK+Aޮ?(gcX2h'e66bLY F VrDe(?mC6&v AǢ8 ,Ӈ&*0]`_osũtMċq\8u\CK$oK0C7WY.,uІ_o:Eod_Bn%qHFll`?19q2[gpW |wEٴ"[ZoE'40o鵸'ޟ43 cijz1GLqY~vSe?xSqӹHH;q%;NBZяl?iTiZҷ:J0Iֵ}} Fl ,q4yf6kjjOe{w NLFøYT]Pz0tº񎷒q?30.zOjqjT8@ƿBxQG0}γ{޼%LE:3<2KwKOr}ǯntA903駟>L;Ǘ셷] ׄ%~_P׈ʸql:aСag_dMf :"B|/K,y(sKX,i1X'A*O 74*y&~Xц>ڪ؄[/˦O^ʸi\GBb3! k ίM~ӵ}d`żsȐ㮳SJV'pxM&6Wq4\ijqe?^ʋW"Ie<.Dr}kתy6__}daZd@"ߝ[ڍ Un4]؉*O>n U)q u>?nz$ uW?u_XHug\i0 ?+_z1;{S&?`AmvuۗtALWe:z pDc{(r_C [&ykTTId FTV$n32ʪ8mKv({'p뭷V$Ir6[X| +\rrJ{}QĄ4oɛ"K%Q^ZU[u6>2!WO[уW%c_1g&7eizLjIX"ƖT\ʙ||]ƟF1nN'fE>~@[k AZI)GnVZz7`{½n2˄W\1,raS _Ð/X@=dS뗷g^Ń?? zsmYvq㪸~Wi1V[M?@#ቃ'b,rs\ԏt뗀#c!$ibj?cmE!dkB "e Q~X_^3~Gҧ-tTo+#Q3e{hiGm>}2=db[G%TRWGL%UJۥxeįB'FY)M>Y4q0)S!@_Gb9.z,W4s)uO`KqP-eڷXQB_R!*2F2~#e,A"`t]Heի\o6bӾl|p]}ay9Rn0tzg_~lKW]uU8o0s+lShga=TSه> e:ʈ6NܖtXXQ+6k?mgb//yގ!Fd1+&>|3C~K!SENiQLMPP,Ko Xe?Til|eLs}M5,B(IV&W`}L'eWV$=uO"ݪ/*\X̾{\#?4Mq0UʄU:hg7؊AzQ TٳDTGv(R6n#>Z/c_фeJ"*zjJ)G}F6 :--e2vW%r[Ԩ**a"܄eI"Yb*c:t1)J 'V1x5JK!ɐ-e:Sb+{t2)̦tH]*mD-).e2=* FYERGE|.[)cjO{QD0zadzfiKzVvP"U}Sn(H|;e\}x!y&×4 kкEЂXSX~Sv? 還6P$eQ? b^|8c1dC;qvxMfg=X:Xz돠dͼ^3^]>k?ы_8YO_pw;Nekab |/*yٟFdۤnoba}X㓶+~*\'oVʵ+Y3]vr̷|D ;Xm|b= -VIEf3\dʰW[R҄ԶnSO?O鬇zh8sW;_9ےCRYg9[=|5ڧ+|W^1yA{WXuUkm5KV%Xr%IM/L#Lּk>KzbK" ~p]o_Cj~G:%ʔ Zybe|qq*3 }'|<]R?M lYmR:>eaKK՛QżҮ٘ja$CɎ#]Tr"(Sn*qmbG6Ou4z~al{,<ᮻ2/]FaG}O$/]7$$c7h##ܨ]4~s ^K_>μm:\*-Fyq~71*{I'T'>D;k4um:uL2z:@;P]MkMFbFr{NeǒdK쐖Dz8a݂*U=H09G<?F'*2'`P#:b:('H?[e[jO5b Əo^?>@Zh"k5ְ7j_ 7|3YSZ{8SlP F izò..\,e)*H^^^ןN]BIL / ϥ懀NO{[OJƱNz-wֿV3߳"`nD}9 !)d*R,;=iaGJgbqG FS.O5\p:~`оБ?rB EGvsytW]SNH劒 ٩xꩧ?6 g? ZGjG266u}p;&GIυ^h1L[ly%l`fo|a9 syRof>dsO)iy,HQlm쓈'},}" |a_ffrI\focNG`#i?|;g(Su\ϻ{cA4}ܼ'_E,q#gp=bSꈭy!Q8S\ :\f?X#YzvO۩^_!md*V^y0\s6^M^?pC52KW>@P6L3 aÌzF0YS׿5lam5/$7vo~/޶,5 }ہn:.%-MWY4 O5tjSlt8l7وrG+52}S)rh<eC|9ߞyCYzŕJ[$f*~M+Ƌ G>~1ӞDG˾<ܷ냳fdoE6_ᙸKenQd**G6Yp4x6a]$ ,@Xn#-(CngSlrp-T_W3׿udB <8G?Qw_{i?u/S;d:_V/zQ)!{8G[<xnw&y(n3Y{y\*O=e-I!,O y"K2cL$&)J;~q4?jm{(ra.~fF z\s1z+{Z7μLgm =PK/[l1|Aqg7놷z+wyᤓNF:aivꮙv9o[/uꩧSiKY3'`k_`>ny#8=7C "_+K_Zzh"il Ib^f^SeUVL"V#__ǯv|P'0U#sue\!zä ^;,|_5'ylb׾5:B*\];ijɴG}Ԏ+PdWkCY^Lmez,2YyUW]vXG&DS )n*q6]opGp: Es@~E2y2Uڕ KOcѶ%,MD !V>Pg\C,SFyyNZ̕S]A׶zWSf]YnN^.=>i=eiz}X.'|$:ͫ(<]ʱV^NJ[myGxpEٙ5%|~g̗^z)uQaԨQF@VҘ،i{y@+#߬.eɧ!"#8#|HuD"yv^PRGZ3Ig_c5-[Y%%e&-[Y^מ#=? /uȤ!IeO˥uN.SFuyy1c1|AF*jo"p~KT/>?)xlСC=xCفl_ÙkV@T&SZ(ǽwGp]g)y$ʑѫ.'txMZRTViXp Wǯ` ) N۴AǞקutByiҲ4FN.+-sfyP u^x#e!h=SY|_~14Tٳ:˦p ;D'B>f!~ᡇ I%6t˩KSWNR}O;#8g 䵔)I^9edQO igEBѣ?M@Of/َ#u?`FIf^NyS⾴/eCy6a뮻&Ki,ݴ,MV,ҭ<7tSxmrl9_.<2UĔd-u^|W_ Vj'[VC#8#4B%"XkdiIP$BI}UWJsn+l?GCMlklEVR4L.#LNYZkeG0ivѨ}dE]a>E}΄7aiO;p7Z_waꪫ4XdK~ن?T#QXɆB|?];#8g`QWMs9-&K})H7`xe[CǑ6^[CSx&딥X2G^ruL` ~󟇟aذaZVm7KM>U^CL<yYi_7f=wmc<9.J+dh-d"9vX[]"DK/P+o}[v$Zj)~1bmHSNY{}A8?5V/=GpGh)h|`oƐ Z,5]r}o`G qG?FuyyOm(L&Km$Nfu-nf .%1û[VLeH,ǏC=4k##s/xy5\a5MMeǫɫ+s1+o?^yނd{K륓ڧO쬺:;΢؏hzJ}G@;K/5mkTFyL}c[vH{a kGg9氝db3ϴ35!oT'Lu#8#[rk+ .e d0&|oၚiDZHrBJ ɦrL]mV'ݾN3GY`z+i-.*mrq7 r7~׶%uW(2PWר,%u)kfZQS9y#Ud><ãא'xbs=m3i/yjGMK,w6鹏ĄQ"Đ{LeСCfv']Ë/N:$z7wy*NLGpGh'&>ħ@Zx2<,cp}ǯ7/а1LoGjm;\_,O]J S7/Y#9m@![+ʰK /P2d|衇l7S!u]]]F2!8"?lB!҉.ĔA}LGl8믷 ~FisN:.Url~0m6Գ < \;Ď)' x:?XGƃ#8#8E@:)6RXh,BR"Ů/$f:\Z%9N.;dž8{QRǹuQ)=ÌLBНwyTj[jCT:K.-C}©.b ?zUV [n턋'StMs9RY|+;#8#xƩHc #->P_5\P܇Ӗm%PR;"ڴQyF6d~Aݬ.mO J+dgNo3y)7xüLulKt36ypPnѶ7bdZ.rLeXJ/w~}iT~Wl}(;uY-SatMɄx~A35cU=\#L3V{pGp" ZD$ OYclI:+:/=_|ƢX #z'(KOt#{$ 2CAZ͉K#{*oֶtK."$ bBBK/4\~_|x@f:dȐji}<,>Ίfml8dv6zcGXp?.SO紫+lMƫ QƳtaH5Zrm>6:ddR=58#8DC @x`HP*,WPUɕur}! :~ 2HvlEif|_ 묳ܦH^>@m+W"Ǝ'|y_Y잺;Zʈ)40͋}p춺&x0NJ>_giGe!tX2-utXNKgȐ!y#8#8m@ #HHR$d h%O;~q 06<~l';h#Ԛ1׿; x ć H᭷޲Nj0˶yP&[SS2k$@؅)#30Dͮs.KPۤR}]:uAוi^{eGV qC=dk0YMUKve}ώmkg/-GpG5(I*c"M"1!EAr|aZm{Tfձ+I'd$ ϥ%yFd^NjՕCe993?D-6Hګj[۽N7ӡ?xYd{aÈIjo$;rKxFcܼҬ/o#8#sOƇҀɲPe&" ]ߐpԦȥWCtݒQc Plރ58!OV;i?4i^S?C;vX#`K95?x u.ZB6ِ x?&A!:Yg 1} 6lwGpGh+;"2!ZUݓMW%!׏ d+KǏ 5ߕ}dHQu!"!I)[o~OR6O?J7UyrHL[xl7tӰz녥^ڼd%SbkEgi5Vv]۩ӭ+^+IS|PsQv FxǏo]`v믿^-/sGpG`@%Lt|^)brҩ6M|е)(ezMA"Ax4/H;#8#>"[ B(/ ,[w}og IDAT7#"/ov5jgyS !pZ> tW}/nS"sȐ'mիu6n-4~:=F L}dmݣ>j'α6lc`G$gGA0Ԏb )~KVd"PVrҩljKz*S^vX2:_><öbFW>UU^(cJ*Hl}e*e^tD8K^zR dH3,xNOnpGpτiT?Vc)=r}ǯ&Jq_OxGK r-~y/]vYt+B$6{믿/>l#F?я*ؘn0]*WdU/=bɒNCj7/'/si;y y]GNuzHB -dDuL), &f.Uvc=fPM&dDm+vZ_WF=r<\>\&G=!)J6|e)^{MmRٺ?^ $r8 x]PLu+\YGs5ho+}pGpGpzG >/v?2*hRW=g6LU) \F}fKE.u<9ĔEuyćvM{k]xFHScE)RSgѨQ9v 5ĩH[FD&oQz@ca"!^_6I"@[)5ASSo%?,0f O- !pGp!`|֨ DD)K9\RL5N2~Pl_(r˂ idRS3 <xWZy/!z0|cdO>s Her}Dwq ^7Avr7+OA!txMuy?(OmQgVHbdk%ԋW^ikC f$7܃Xε0 !%kz9faV #8#8mG >B ٖbB2Xv;~m?E.Ea-I6ciKi $ϋ<ڤ!{o8餓:cd)Z &$#l;dS8H뮕GY}pGp#K\S؊iPSgm\I(rK78jc(r)Gj̘1ȥtq7.N;ͼx~#*Q,[+:uy^ra-(!x֞{9 qNJ76'(ί#W>k&DmE ˖\]Y7"$;ɮx)ѯAFx1qUV15JtDNYoK@*9憱e:,ר{8#8@{.y3©V c\qq/ 2>kc\m OzB c<[oMerTUt[a򖗧) ^RQË5ma饗5+esɪ-ũ4-yԋR>-J Ar#ukޒ&}ڄ+a>{c=l7`5~8" iE&ydhMj %멶=vGpG` 73 $$Z>Y=qT_O1 QK]D;S /ص4"9"4xe†: 䒍|ljCI2JKFy*V"O,IB[n9QiU@WZ֨_u^om'%/ WՏqƅW_}^x7qWӟ4uQFټr׌V$sQ_~.Loڶ#8#8CΘdUB1-&ie|Q&ibW3~lȥ/֪?ab} $DC2l`G)id#TFeVO3sLٴN/46V~f][iJKNq/5^XGuWԾbR[xyq ?*p%[y.<8ls|L7*RijcGpGpچHjUް(rroc4@E.Ex6A EJ&` LwģeD)of7o'7IJl(zΥbZA`!ōlTյQgN.L{e3&CN;v(KQ'"JD6b:~g(Mچ2H턠+}*pGp")Al:5 FrbҲ"!li;~MO ["\d1`zw]X,R "Xܭv $/UiS=>4ʧ6RSg}ֈ16#}bLeU&9#t#y6dY s:xx F'^k}c!G`o-b6!y.OKo#}/wGpG%$&#A53-+D@]@&t6E.xWV[|n}qT䋣5#蚉 2J$:y>]ũ]3-r <묳«̙g έxo6[5B '4YHkiJ\KKƄpGpI@dI0'(/%!+/lCqkwKmԮuyLl8s_Pbtr=7mSi[i/pLuӼK4Yp&h6u8#8 \"KeB tX~FGIUR,(r '`kϊ+e^q 僨@RBJOR,;Z$@Gtlu4ݟ~A0Mv-ةQP{#8#8AdI؛o:I 'aojUVm&e 78'ᦸǽ(q3>EU؞DGK;xvfK/dkP gy&1FĘb.B!"5++m6[kr)ِLn_?|\n?N3ۮ&IuZ#8#8F@S7 J(oS^RC9!>V:ߍ㗍 L0~Qն9œO>6xaЇ~:" Ҽ LzGQTKE:FZtjKei~#=Dzs,X4 騮>i'myZrV&丑;6i/~Z[K˺TeXb&ӡYk4cKr^&kpٙ6GpG<[_2HJ؈BE>UC)k{GKE.9N;m@&'!D2y=f Fj!YHsVhEFvG6pgy#Y^wuaȑ;44@*gaH&ē<1}6 ymg[_cWKGpGh bLE,1\ES3[qro^(?llWWWa"4#4L=lV"bװ ^;N+u w?Mӭ`7Ks\7H<23U]t_= veC $כ2GeS!bk 74{ϭ }ŢΆ9#8#;9̞`BEGm#$Yɣ2w2~J6EE.&H{mGrx"@=C L2dӕmՑWN'Sz:묶 7`ڰ!Ӭ::/+W]u9{e O&/~\ 9眶oz-q30ɺ܍6HX&0GpGpzE !'e'>-(IE =q;+6NjO^;\@i ixw @! L?뵂T;HD6D?4%$psr)[{S;`34[6վ(\ҿUVY%0{)"Md/+/ydScgM\ ;o`4H.- ؐ~+'|r3hV*wnZۨh$Oy~391A0gyf#쳏Mu[ ڕ\Q/+u{.(zwSGpGǂȢŔ0*.wDئŚVܴ ǯ?jGԑ䒍V׿J25#)>l;iXnlmdkBS2ݺ=v8}+lk$Y'T{6vۅ3^To$tTHp:u5U^v'V1J벮}Q#SZ~7ɿ曆-.|ߴO8=8#8#0ǒ2uN#Dw,BJǩR}ZNԈ-rT>-wa8 I.ٜeu _|q  nm#خƦ/y뭷ں\PM㔘JK*N HFvTQ!l=-x!̃(땧?L5馛\TF(V b#T2%\bǍ %ilյ\`7?chk_w#8#L|@җHʌ%rRy,yvRyǽncHrNJ;mr1IbJve(;b(GO|ΎIrf[o%%."DQ֛-ũ-$ %LӀloA8/o6 eY-2t7|ݏeBz2${SP]ңlh)(r)kd,bbT`25gLd=&lV Le&*^3ەftX:*W^q^%Ʃ SNk[W" qJrL9N Y?~|5F:4RҜKKf;ӲӞ?vk'mf,3]>K_qiGpGpڇ@$y%i3>WɘH B%K=1));rZlz)Ld%^aÆѣG%@p bf#*XH^j|_C3)64ߨ=^4d4A6쀇!Pn;⋩#}fl@DYLjG)LeHki8^uxY9r:;i=-#M9yX)_tMT2v IDATַ"pGp"?{_Ɠr͔\!x4?X`lmOguU~X afs ~(`*du̘1Fb[w-a0ė~1"Άk0oK0>ɮlx8#8@{(XV eZe&(v}o cIt4Lb " ؈O<1s=i 7ЈD72E6lҶtCuZso~# ,j"T$dioqUHz6Ȱap(k`?1m; \sM׿n^g]rԨQuH#O!]~\=]Gdj{G<3lWZ~`$I}KVpGp#ddF c2%eG D ɺ׎SwL m+pN"/B:?x1C[囥M0{K\.ϧL'ųF_XcjJlf6diW\qmģy֕HFI>J_%e):!lJFsر Lem2M&lb g4A?J>نdwyvMe#[#8#8FEi 2ZG& *#gz"_tǯX)4qѕ/֑G: P~7R!p&^>;lVqSO=ek7BG!-WYKOgtgͪM2Ud('uVɯ]yC>$,^uUF"]vY#x&! "*Bz׆?6' .h`dga64PQ\>0ۏMԆGpGhUAݫswt޷V{{V $F_}}ycE./ W_}u}rK $W,eK&{4'3LuWU=K=XsAÆ(#`0k ȶ`51Yʕ._:ka+oěCUA`]ǿs̰\E?Dr\:)[`A ɱ]Le)1SBC.%`Ⴜ`lDPYenb&K5\r8ю;3aEs+Swx>|H(zvءڄk\ ?cNYWZ:r8!DRa>N7.ўZ0=C*%jDj#`0F` z%&E_ Dlq~WiTOX˒tIH[iWZV:Yʉi++ w6~Rxirk_Zя~4!{ه 9tK&KxblHӝcO"'bAP)W_?iEV/ur~2A9GqDmDtAq:,`,Nm#`0##+Z65ժӚ^%'+×IfS9Btȥ,"%H&wyg,xtUʓILr!{ʁ6,])c=w=Oj>O~̗%q_KBЁPRD!Uv[u-.D7D.=LS|&Y%ys:r~5.KdA<(;S]*Wiҳo0F^haKB֌18dnb,;?: 1o2~ 8{cI.K2$J~׿>Nrlj9=+#e!{ӥ^Li2]ab2}ꩧÜrxW~^Jo0F~NDrڂIk#ud)ǝ?#56~2-frw,ɿ^2wDX~ _Eu} ef\vNS݃\:C:ogU${n~71*X8{FB SO=%/BRC"y8 9 I8f…Q'pWmOV=ǒ\N^""l%A$DiǞ7 ջO cqS!ܞ\w>HRY bUJ/qygC͛ JiɻpEw=yOug_ե^. #tt齅EWyWrYΒW$p+y1Uu*# sYa#`0F@Jes8O[xM-6_|6Vo':X믨5Sd$ g(8Ւ%Klq ֻQHFVs{r]ʯ6)M>VNo} _YB;|~J 8 $*DzWN}ғVK! 죎:*-oo USpWqdaɅҳ/,#-p ae*K9A&zEB2r7F#`@{j"< yVp~Щ YINgj,sG.yP@do?8+ Y9iuGb{n{Յ^XA6Gq˜:״RO?=嗾e/rg&N>#`0F D@$_$)+) 9цDIkkdo1hqZk&nYt^2.lj .:zlٲk-[o5t8q ۆe=#<|P]*o뭷+^s5qaeyok č,(n 7pPx21U~/}i{W>GRm=L~J? .,ʺ7F#` ?Ch \@h[BMS|9/ w\7q Sw{*lq:+D3L~fҝ)B M,|0,3W:de,}1!`MNM, b*˗CBw^W;S<Aʁ8r}ʉ'ur%K6h(R|I'IÌ쌀0F#0vIJĚ=:$}voVK7%D~cG.2# X WXf‚=[%0 :׾6nJ,uÁԦ\W;Io>dm-vp e{P}G}t뮻VSN9:묳*5({Su@סxq9ز?yXEKY7y9+W8MozSESW~Oz<,E^Ld|rHye4Ae/ܾ0F#`V?G0HiiuhQ6ڄ;?@ O Xo0ucE.5ljϲP,zx;ׂ|_ P @:qo~A:YzŌe\ ҉n r?z$Wa;Xr {Fk7R'l&rw >?gdŒ}I|JJsҥq-DK|ѢE bկ~A[A<3f>J|wV~+,8tiGԇZW(uut$[d0FՄsݚ4sz23n};dMg+{XKM3ټ+@N$Bzqb+>r`ЏUfzג|BSRȡ?,u2H|e̻̗%<@%+%c9${}ē@! m'e_~C5yMa$׿jw=`xA Yfd0itY VY[bat[f0FTT(W@6dOdkd:kP,q-d ?q@2PhZ֙lHTaQ:Va|q!b7eϟ??JlT~YW2P/2\N}׻$e| O9ic-m, L~.2H.? rhlI T}#`0FddrjX2CքCFŝ'\B p$wmf\6h޼yOD˜ Bꫯ$9=_, B?2יۢ&J&ST,1>Gs![}*|;ȗ,'|ɻt!恰cŲ˒^8g!XY˾~#we kOQnfg'ڦ}#`0F Lo!f>N8du EQOt%dŸ]!PzK&._g1Q9"lʛ$^K KL?Tj ϑ:)X(!JfDzЭڪ:cZ!?,mTDQCrխާ#]i;)ozT)2,Y io~'!X2E+%JNK.$¢+[XvK~ڠv.t;#`0F$)M^8&a$^"g 3|&_@%,?72OY6(ܥ+l\.9.,r!ACi,݄8s9ŋo=r* y>ErS:VOYyA 8H+{IKcoʈ .CST2SW:qV_Ny2 ^E[(VK5tMAHKH='nfVU?2$=F#`XY$d1ɑaB פ' ƯW?SwȥȀH@["]2f*[m|Uփ>6݁d^tEAXʉXCWeax}q"l&FH5\'bci)c8SX{ d9]Ug/ۤ$eQ'aoiկ~'Bʹ= UB:9SyRi /:#{ʗ}#`0Fv\5}enSǝ `o%V*lX˲a]]2ْ+?~K:$N_ݒI*}RO<>䓫q: HN^ lQ6qߥ\d:e~aN!tYLâ˒aba%Yg/3W~JS^g}7F#`AHi#~5fN*Q|!1~m}ztcE.5XQ3کwyζm6qVJ,zkYrBPYr aJa,Kc?X:uV!\ue:<]z3ґL~I.=o0FՂ&p rHl+'1dh?N-A7~?SV.2 28.dT9kC:문B䮻l;3護޺ ^]qaD=QNW3[ly--¬XNޕyo|PY#`0F`dڥ ihS"|YItT2N!w~7l#qbe( Aud°dTk6dhvsW/+ !q~N:nCn/__:estaU/rڀ^R<\gJ}#`0F"dtBܤs9$oEPh<=c,|'&A}F;HXɉ;CuW,=3w9O^uU-A5HNű|{;{,XP}81C]:ڤϤK>乬نs^qcP\uMOwӕ4#`0F@3iq9pX!#\NX&$r~ri~?1*2#`0Fz)dl7S08}o`3~ Sݤ'3&G_X˾^r\mIDAT\a $)OyJDzad3 >hÑ{%}qL\ եۗlP}r0F#CFٸEMai)AOao~ԓ?vRHSq|ܨCOWtr]aKZnd+}n]ڍOA>/ov㏯8]=SYť#e윧K_ҲiOj?S򐠰?FD׉?t^-Œ0F#`L#Y"1Kbho3\od'{k 2F`4䯢`0F#0@t0OXrdqnҝ! /W#hLKe z3ɭOz3~(Ǘ`)Vf0F\fykBIoeM>M \?,4#`0Fu W0~m\Z`:VpoHHEғor.#`0F z~2y$WI 4D:k-\T)s+w뭷^=Vf87:S:~#`0kYڽ0ƅL~CzC:`?u2f'0Ҟ\{A*yW~{Vq;bL1p k3F#`|.B^š$E4l%A Ư!s?uoUҧƍD.hofm#`f@'/a^K9c?X?pMի^sPBN}'?"R7Tɿ_C|xF^ŋW'pB?zc|a궮X` qu__viaVs |Ieg0F̉k3dO[yWզ_m{r[f]2%ie.O!PbS_rOk﨎:7o^G?xaɃᐹ&mzrCԻfY|yT?oղe˪{キ{*%K2XBUO~c[n2~X%/k&r7F#&X2OL_i|w\JX궥S-s5٬uêϦ6߲z>V[mU30h0hb`5*,m*zLhɭ2e`bPtMBFf~eIF "@B$53Kc' #`ڎ|ߗse}3gWqy2dǞdꃐ<ׄD`AsRt꿔1\0!L0&4d`hIF@] r ̖JA?`dv#`0~/#`0k"qg+ 2ear>%:YomA9 9%9& )%PE.GiRcBCSÐld0rㄇ@g05?[-^w3F ͕;滤!ӏ_D.5?k6~Ջ ~&|H&}4y:lKG&(78hf,5&i2.}#0ik0xy'Ѡgpj3Tʲo0F|3{<;}FK-e gB@sEew{31dC4G%T_N_Y!yV_ah,EG~ϢJF@c`єuO4pn0F DmCn;1?&Ɨrn2&7?@kTOyfX_2mȥ8/@#0 t@`? XL yj3N4^Ha囔wv;0Fwzg^, aY,=GbGE~Sy|EUaXyN6gb3TXKSivF`]B@5r!ng0FL3|RY9N佱[<hΩy|攐L"ȈK5@@,+-6k V95EkpK&_y#`0s\͑ gKz3ω `^TK5B_a2ɕ׾X%S<;l0FL6 -2ٔ +4l("leYLd<Ǎ"A\s#`Xw֮۬n-h#}4e#`0F#`x"Bsq0F#`0D}\n0F#`0F`<0ŭ2F#`0FL&qF#`0FDr߷IENDB`amqp-1.8.0/docs/diagrams/001_hello_world_example_routing.png0000644000004100000410000026152613321132064024116 0ustar www-datawww-dataPNG  IHDR3=iCCPICC ProfilexXwT˳.iy K9gA% * $(DP HPD ""("*ow&TUW[3%<<A@Hht~g> 6p@kmm}Яq_u>d {zGy>^ `H\t80f_Ɨap/=V3Bf zzx00X˟  3{Q ýhx؞臅'DG#g%%''#+kM{X#UM(K?2A%KX􊉌K 3` P:6V87 ap A1.h '`L0*X_.AX!v!9H҂ !sr< ?(Cǡ (**&tC3hZ>C;$`Bp#i BaGD!",DqъFw7Cp!D5mZ" 7mm m+:j:!:]:7D&a5zjz2>=(} m)M",CC&5 odFCFo3.D>ыxxĄaf2a d`4ĴȬ\|yBf1a f92ɲͪzuuM͇-mmݐ==,CÆ#C5N&NuN/tF\.1.[C\6yùs^a dzK }̧WoEG!UH)8_<uA^A ÂυT zd'I-[a6aDZ"mJQ 21Xذ8B\I<@L|T-**Q)1%ԕb2J%AZPE:GWLeYFYSvrbr^r%rO FIm|.(L+-O*(PRVTSZQTP.URaRVTSE&Q֨Q]R=H[ a  9->-r9m6ER{AG@[JgYWT7P=H-}5#]Hct!CFCb×FF~FFƊƇ7emeRcnlzάl\<ҼaajqⅥe-+`ebujZ:ºccmSbVmW{=3"1=t5[NNNsę9Ry@%WE4Ƀ>vp vNNqo@{8y\NTR6=M=};"}~%W,-,z-z}›e嚷ro;ni5|uw-="?|X߿) / _z67_~ ;}ʷݸE?D4b/do/I@g/ 8@G x o0vUD %Ƣ?aVSTq[x4LkFM_0EabNdg]fpr y|HYcaZ:Qx&^b^r\tel# :JbD*Kj5lt$tyX Q?5362hqDz5kv7n:695ovnvi:z`[{Gs/{4,A!ڡ&a>qg;cc?S'%*1}g{;W6{Yr2 [XUk׳om9mzv^6]=N.n{T7xje<87j>f7eum}s ek>|b7ogW޿Qj]0{_7˿rVn3? {qLQpߋ)W/TO`A'A$"&**&,N2˲r} UJǕ#T) hjlk.jhw4^+/172:i/$RJZFԖl'h@rux=}ce'łBCUôÍ#l"ݣ£crb+nw% 'Z=}KN,ԞeOǤS2,2Ude8>{!EgӅES' _8T>Z1Y9 g+;ըk}4pj0ghHhXx‰ɌG=1ͼӂb딥7W;̽zAܺ'FF_7o)m~!}gAI_ɯkpp8 y `kw B(@ a.ā2\w`l@8APUC%!0k"[C#!ȷ(ARzk ӄz`;ب⩞SkSWuQ{wQAp ]*=$#7c QĴ̜BRΪ:̎cYe̵]c[@BZHlDp{& mIT,+Vl`Es%9e*:ISZK[N'P^~XlIiYyŴ5YUؗS\,dq3ry{f-GFVGbb_' G}!:YS~fg_.辮\ƭĬ.}pY۰vyq{?w?pD!t qZs4/$i@ǘbJ02Qqjf>Lӈ`X#Қ>ӣף`0HaH97~;Z'Bd3JQU1بxDVtLg~My7_=`:__m#gooro%x@W_zrɈ}5$lAB"x pHYs   IDATx UU jh.VDт$Ա~` `BTP( * s{yof̛ao{ۤ @ @hKu!@ @1 @Z%VT @ @ * f @ @L @ VIU6; @ b @ Jĭ٩4 @   @ @U@ nN!@ @>@ @qlv* @ @@ @h[eSi@ @@ @ @@$оU֚JC ]!/nwKe^aw_ 읩${t ]񯃩jZY& ؤݤ104M$U+d{[Ʉ?}Fub;KȅA>#CƍM}˛dz^]oj4) RK$e&+.KmmyY&[%PvZ-rol{]dO{$}?'nx:w:p;t E׹C^KNF'HutgUCuQk-BS/9Ac!F$P׈EmIWm^&w{t]/>Չ@ q&+4"9LDH;>Dk6#dZhl5jk\ Po,[N[oU&OU^|gGTw`ۮ5( N#%A]P$],:7$#mK͚1)% }L>b~?&+{d\!4qҡC41IW\J&%hRdZt1_G o@E#ӍEt!/ ̚{z<#5!!В 0>ZrQDKlSgZP(* D'UB:u ٚqUgykHQQtz||p))9Mi}bdrb).0Jd KJ̡}/)kLW.#riCoh>}.K?=ա;SRUV,yQ*W+{e;?pO'>f>G4|5|k۷KOw/uCjrbd՛[l.=cN%g>LV)7o||L6^eIO䀾wQN<_}O_t[e{ѡ2xȩ2r(9xW46>&;eȜ' +Pk;4XNW*;Lz۹y%T"g9V=$G=wUetJs]{qs59{KVo?H.rцA:@Է=|:XPCNYyOL x}1?}'?릆~NVT>qKm^Jc/6-,0D]~}"쳲bf 8ѣNs'1'2C ~'s]!op8w9 qOi72cgӖ.=wlsh&EL@ #_M !V1=Ҳ(;*fdu8fJP1}|:ؒ* qSRK7ȑԌP:aU&춗+Ǫ/N<f}zĔkKlZ4%V"jEVV錗)h*R5)ρ)k^`5ck斥klR/͚5L 15U}|L&g"uZ7;RfLũ)*mmmSßANS7hjeNZ)KۖJBWlI;-!-NŭsDֆYYafq} e(\,OeJb3#&9}R?iW7_2"Uei-Q<1U8 [*kf%ҌLV>: R pd($@Q̮pj۾JSJoYky*fNGVQnJ1O5",8I7/Y.k~[s%l9?/˧J E ~:%Hxr#erG HYYV=r9Wop0 1F_̍e戓l'\:}\?{seukaӯɗ/=;Dp/\Crȩfc;nDθ)#Ϝ*F>4 ŧK߱\+䦱G5,Y7g2ʄ *q,˔Ԙbßt@$dVK p_xMrv;eŽR\zd:Zť\A=eu:% B @eF=8| cڤjg^%3"IU?(U,&ɬYeBF0)̈́)ӤR1Y~8H;$ej 厬w!cg,aD=ĵP3`IR6}i,Igi37&Gml>)7'#k-_$4@%h[۔T=kvj٩iwvcfJ@jIIuMv 3|՗pq3OIj ;Wnr nڎ;!5%vijx¬Ԧt)BmYPפ&FNL 2?t|شYp ,8*9htiKCwu[*wH+ ;JU/Ȅu3֤1#z>=!X MNJӤX>nYڣ ְ,:J+ץ"4˖#>Xm\_RHG ջZ1ϥdJbOvTLxH驗biQ _|B-S`@` Y `^mQ"-NͮcJ4<"G'5oY}V'2bF޸eQDE]ϟriQdLWNu 8qsq85<]qX;IMnI͈ ű@Z4T 3FTO,lwC<_xz|Qdxjx\c7n$~s'&xir)^D 2?,+Ҟ*gU*ss:ܯMQkDco_Ln-KcϟWaJ2-{p4@"iseEAG`?x)#Hi8\5F-*YO#a 2۳#8}\ avpr-gDJ%S`sfXߜnnL93,nT ˡq3Wn5CM3k.ݾqw ~iy%D?;-Ų0C $@FJhxtڞ)~i8놅ٍF7"z[6_gcO Nvj<겈{^Ǎ[dY2vjxe 9m2䝍o.yaiBԯ{Oe8;P}AT^Arjm˼tB4n{֜w̷1c8Vfc5;9n^ȿ]}u[zJjω@ "PՅB@Mަ/ڽוJ{FgB¥-=uHVтmZ k+ᗙ N-n[_\z8ԫt=/,-\ /nf$S(lsB`xSph^=$Hx{310u W2e|ZJ5}wʥS)Sϧ [KqNᵄPrx?U!h$Hcg$-51k1\soM\j+ז5 9l7j @pdP$IN`+OusyP;-<,bn-ag2w/_y^CN  O#K;›Gk(NNOIwjb—D)2ebɱN,5|9s4tWUɟB*.Gl:N9a6GewݘTEMCgokB޾Bnti@W\bЏG&}>vڧ[:yH .@4_].;eSx'c ڳT͋XSQkmo꒑ ЄrUi ,Ecj=2ڪ ԺTˑ'FșmYS'] B[{Ȃo{hmoqyT\8B6_< 1d@͜Cűg Nzmk>}C2-jnT9NywDHZ>C.'k^Ylc5L;m;ߐ?Nj)`X8۞5fg3U[6S} ul57#s}V2 F#CIB@fʋwlZ7g̛7O̙'VDjQa^XIY] ym]{rc2~y7jk)CaΗ'|6txc1Hq-Z|۬'˾ ǫVmqii舼p\?5y-_⒙b˟r"᛫zzRn_x1WϛXlE-kZvշz!4yp..Zom4|s%3 ĉhF*JHG!r#+]R;v7y%|Ot FsYR77˜{g ΝG|z >3r锋'|O1#ޟW3ak6q][)LyY+ʳ+ǫdcu(szP#|: -L8%TŸ)x uyFiᖦ5.Oޗ^&J|lhdn*+jLx[D8I!@hK?Z8}UM8ʙ,^"s>QSV.s:~,cw$eŲe+ fSFG8F.,2&sB4j$lh%BԱJsLYǷ._oVt1O;"7 tJyp yw{;)7OyYn^|Fc]*gz\ΘΣ1ڳ,c8.d/A1WowD:`_"麩9[=Pm\bB8! I5 \ sLީKr7 5o:b2ic 7|Xr~*3eCJ3exkd(؝<Հ,}|J?\n#(c\SO:J\$ENg,0Dszsg}_A&䶊c嚻Ȱ!+6j65)x{6.rH\-r714U;7⇾&f/Y{M>_ʚƶ) aK'MOM_\߈uJ-ev2iLOm?ڴhZ&~\'NK͘1#U6eR4[yϊ2s)M-*RºZ*וg)Ԇ, l[mKۮ%e-Yzr鵇U ReRS&fj"{m;R'5'VޱbF5'MIM6%5$;6ߒSe(2\ v/g鄉)ƬLHUȔʙPgoI0>e z'R=|^a錜º/'&x C%i3ۡ z tTd Gz@Uuؽ} 2{Jc}@OV.*hOE#ԇvlmF1_mkf5llY 9CpW`$|-K#/9s`ĞΈlԀw.C!9ԅ{u ;eS )撉hSB/NYbܩ}|4~u,4&w1?kNG{HLՊtN|֔=d4Yn|qh{3y't,S(.Bg"aLHHZn YtF\2[ IDATq$7ޞRnKr tyNʧEXY"E9׏rEyj&%mE-NpџzV^ضNfO/ʴYKeSeGmI$F$V:A͗> oސ]:I'#t?00h Me)ORKVY|,+K:d|12e`ov홵J;կޒw6a9|KFA Mhuq҃Y5o~D dIIR>vj(r=rͻ!?Q( ' O I <*+eCv,GBIbleS-:|uϒ)8K6%he.Z 2ZZzBZ"ͲKd}A'Ȱ>RKB @|kpA,po(..~ţesNQ(@ ۶  @j @ @%@ܶf @ P@ @%@ܶf @ P@ @%@ܶf @ P@ @%@ܶf @ P@ @%@ܶf @ P@ @%@ܶf @ P@ @%@ܶf @ P@ @%@ܶf @ P@ @%@ܶf @ P@ @%@ܶf @ P@ @%@ܶf @ P@ @%@ܶf @ P@ @%>Uf@cHRY50 hӦM$=@q V@:w^H~ܪ  @ aaכm^ω@k!@ZZzB® ^asU@6^~U_]~_ɼydÆ ow @1я~T=P3f\veҧO+`ܾ}UB3 '@IC]UB07 3g\yRVV&C9^|MYv<2zh뮓^ǼPC(n&"C4{mD;(4+++0m6⋥X.9 =AD`ӦMrUWW_"ر)qeDd@ 1qb@ar *{rGI'$=pPެ}g峟\tE7sw%z-N7}$ 0h,^ ֣Ѻ`ؽ{]@رC&M$={']@@A ; ǝ:u:أӺ[ @L1iTZbwe/0}Z =>=p@mwѥK{|ZS! 4 L7 V@$ T(޹s雼zWuTTkeC!  Lh zdZwy[5*@-W\!=^,^T`Z7SwAwsi% @^z7z{2zV' qayZw b]<;һwW/ @8Cd˖-bS]@>BAWaX֦M(AZ~u_G[\sR`4 FJhY.1wϽPܲjBi!@ 5d?*!5z,\ @52u@-1{X(@@K'bWwBqKO D ]$.P N@wU@>VN/@ .z+C!@ TR; h[wS{X^=g@ $y{F @ n?bC qWA*@65   !l@hƣ(0%9$$ M:D xĞ: @ *ĭ, @ xĞ: G  4Տ@#@\?nĂ@cZ25_-ȟq̈@ @ @F  @ ? @@8H @ @ 3# @ $q*@ @@gF @ @H4"U @ ό @  hD@ @@ Ο1 @ @ ЈT @'@?3b@ @@ ' @ O8fĀ @ @ N@#R@ @ȟq̈@ @ @F  @ ? @@8H @ @ 3# @ $q*@ @@gF @ @H4"U @ ό @  hD@ @@ Ο1 @ @ ЈT @'@?3b@ @@ ' @ O8fĀ @ @ N@#R@ @ȟq̈@ @ @F  @ ? @@8H @ @ 3# @ $q*@ @@gF @ @H4"U @ ό @  hD@ @@ Ο1 @ @ ЈT @'@?3b@ @@ ' @ O8fĀ @ @ N@#R@ @ȟq̈@ @ @F  @ ? @@8H @ @ 3# @ $q*@ @@gF @ @H4"U @ ό @  hD@ @@ Ο1 @ @ ЈT @'@?3b@ @@ ' @ O8fĀ @ @ N@#R@ @ȟq̈@ @ @F  @ ? @@8H @ @ 3# @ $q*@ @@gF @ @H4"U @ ό @  hD@ @@ Ο1 @ @ ЈT @'@?3b@ @@ ' @ O8fĀ @ @ N@#R@ @ȟq̈@ @ @F  @ ? @@8H @ @ 3# @ $q*@ @@gF @ @H4"U @ ό @  hD@ @@ Ο1 @ @ ЈT @'@?3b@ @@ ' @ O8fĀ @ @ N@#R@ @ȟq̈@ @ @F  @ ? @@8H @ @ 3# @ $q*@ @@gF @ @H4"U @ ό @  hD@ @@ Ο1 @ @ 7fRM덙iChӦ5zݻC9زe|}vyeǎ_QQt͚S)  4tNضm[z~P{.]s={l b1oh~]BfRPX0 ~x5 &{GYtʿdWdWCҹK,]$]v4B.# ?)]v#'G 8Z>9䤓N;6vIhw/xb/˫_W׼*;>!,$i{G`2sĮw˞=oG#)'"zt޽ JN_k}k񵼷uRʂ~phAkq<Hm[w_u?Hnj%!3]~1YYvw|9ᤃeȏȕמ$Gqt9Fln*ʞ[.^"^"<e+O8VJN?[j#(h^~e,srQa#t! no*NٵT3Cxwkʲן=.]GJF+.D+bSA}cP%ͺkv{}tv7ֵtp~nWUUe={DdK+j$/qvY~@5fJaN |B/r[>vbo\Qڥ^/kS=~#Ub4gwU7+6e'_[vٹᚫ?kٳ1=zIc:u2'v:p=wRαh#ܡaB;ɕJY+ޮro7'KN8͵,v/㎕};ڗV.tHu}ķZ?-_}5yv|_YfgaOwj}_o=BOa="'> eӾ͝c=)Xe›ć_=ϫ*!=)֗]$1]5?>c&n4BN;LmcULc6mΛl{=k0cp~jL 6 {n)r|;e9ԻL4M6-_Y32'ʱ J}z~#3nu'؏sƙYb*NƐ5\'*Hyn,r9c/~bH3I k} G/d7X~h G|p+<4[|}8)//7/= w~J7M-+Du;H*P ~{ߓoM1$ !M[w3+nG| KگlWTٹsgU2! IDATo"t<j%T_?SrY,JFevāOͺ~u"0:"\|N6\ʄ3?g &ߎ:ʥW g4v=HiZI@"ו{ixtqnfƳniw5g3lχUAZ{(O|wWG^27}"9rYSʾ3^'RZy/fT׭X#7>IMf*{^߅Ӑ}V8uh!;ӝ.u2*[Cf@sKU?~y>q|cCi7E`ҥrəg?z}OlUu*gU*F؛p.Bڛ&ܠFpw~ !2o~S.>;%6@A6n(\q"9^6zC(ӫju~뭰]}<vK0d bV ܔީӉW+)^. 'gZnuȶ_zoI OYJz'`vV j4˷:dȮu^ q|5VfsЁ?/{Xw֮]+ lZwLl(v`z|hUp!3C\gQgGPٱc=&G*Ҫ P=iS垻+S#gc6Zo\og=?Vx4VٹSM+B_=Ƣ K{1Ќeܸq6> xg>u 2qQh'{!WwgUԛgN0V77/"WgjfJkΤNUq;?))27Ν2Ju+r'ϖ/t/D150oJw 3Tf.p<3;:.q 7?+6oij7 U%:GL~|r)-g-* ry'oxMyTE aSzvX'q*]ִ5Ү:}CI  ;  IM৚7uk@Ɵ`}}${I>}SΑOM3+HvMխ`leQS,]N \|? >D6Q]wWPL^XE4Q~Wk./_˴n/6t t3Aҥ_;cC>"m *೺Nwv?>.(Zǻ)L0[0;<؍adҩ{GyܼEҽ{LL&s}y ?*w YGBv|Tb$>hh=croM'`/z[.'#f ??@͛A==yÇ g94oZ5 aWN-[7O?/@iey?u3bUiwp& Tta 5M]ءCG>"KVO# 'ב_> SAX[z$Ywwu-{vk8UVִ v=e`iK/<xLGj>ViXzU.q0OhS?A~yc^Ʒ|rgk u;:ؾ'TfU-$Ea3T ֹY Q4D@8^P=]su0ha:/^#>j?_D?W] ۷o ёrӗGYWX0KwX~W6jWu QupXUNWw8suXNuk2UD|b`1M6s-_g.όܼlz? | 0OOmܶ ҡ~ F[:R/Za ATڻ뼡~ V^ߞ69Wy$ppl.5xF AaYȘKdp׺ήýa|Aԟ=|?z=aP.*3s= y#l3=9SYߺЯ#J`HO_=yF﷾nz >)#F _)v9WwY.q[#]_3lN =B>x^rӞ՞\_Kq\c*뮰 ZN 7=s"/}LnHmߔ+=O:vdI#*H_;ͺESlqn~}ygȑ#11CXhGɄ?\.]8QX5l*2S > /TnvpۇQ?]eU&=}+:3n0ӃKT7Nk|>F{y%noDO0{[gӁlUtkюep}?b䡟\?pˌ7b#F9Sioan~=!:jNHu`9Ʀ'v ? ؤ}Ba7u%J1ds cn?G~Z+`W,uT cVم) H}l0(tqp&X;._.5P0}iO3&\w;WRxя~A@$o˥W\"痝!;fY!RaC;j°ww;NxVn[i̥]-nG_'񲂰 va_ܱ2Iѧ!z ßVFnx:]^;*{GP&ҿ/_j߾q[C g}<^S|)}lh ^m s@u_TB1OuUįJx^q ~!-[+.iaU Ěqe\]Eҟ-aNڢn.+cO/Ҵ4QJ׭rީ1*hۗd XGCfz}FB<~@Z~}.LdG!\s].[ @  xzWQ#TG SQ/d/Lk:5JV4>4ʹ6!8VeGi^u]fI?CS&uqpw9l\|EUҳgOz dD[r]msL^vQ/?-|rI\W_;4R(`q@:`q35Qe hW#. )k?60 Pt9?|SDBWjncv~TwӒiEa g5NiXl6=api98(i`[Lp&]/sVJ}3#6B lwp;Bhvgw$ڕQiW  Ņ7sȖWFHϐ?}\xᅲBߗ-Rvg۱ N,ΰl(Tݙ[`G]nx:ۆ>2@[&\z88*jۣ}4ng6xƦUJ[6&=[n9A~t# 'CONV)l=WX8cy9&~eΝ1~, E^,l7M||_[[9p? ñj2gfB맳ql zëWN|~G-{[*O0nD>N3}LMurªvݢU Ů]'*ޤ2èJȴ)p޽,e])#o kp{DP!8̦^w 4*#dw֢ ,ݵ1[ߔ>ی3u fPMEgױGww͘Z:‚Ik|U&YClmwWӁ]r AmzV8n֩{ uWOL9eecu' cv lܝY.\Ʈvf~>.۝ihZFw;gkQԥ(wye2Yw--O{m%2cOe6. ?Sk֬kD6kX3>T(/Ͳ7ȜpnjU%{֟ڭ0ķ4u6χS:iǯ50Qi-lyj|}P/-T:]S&[H@{U?~m)h7Ηfr鄅ZQԻ-R>wmFԙ Fx+]QҘᬛӬM׉ fuķxgG]#D%g7 ^j]N:EnzB^¤O=`B gX]ie:]'j:S^]Wwz&wvӻX@Y/R}]34l|U}rgv#>S_c׻{OJuPd]wk>.E= sYisS! oM5;1a4#Flx\~BPwUp@98M&?;}D;mܥ%X3u5ꎲWZt:&) okMYC#`7%L[ks >7zW:k(\3mjl7[gB4w ̪k֟a / ?1$z?׾s[*//SI_g}Uf)vN9}Q0D hZv^Rs8\z'Xj]÷mkvχ obLmznؙu9uqqW+.qxO~[> :)[}L{=կO5eƙ]`V:~0NBOJ݁ձN &Ƴ[Zùҹ:!c[&Oǧal<gMoL9"SqᲧK̙3mA͕^+q}o]t'esNq!抲9Egf¦]+ gQ fufWw+ŒffWOZ ]+?-+9o;{74&|@r3Rh]T lA8:tTwSU6,H_U>M.|W6o |4';fa\MlX=ܰAd-ݡ6A{PG;^24G] IDAT_ n:z||n]%KȈ#Lj֩kW51l^M>7l P3&+#3t 9 Մhh5Y_+rH v?V7j8,MȇQ*V=w#'Yg4"oV_ʕr<ºxR:\{sK[^|.^_Δ.hiO~j~N/< m6*ZUPnjsLQ.o90j5M<+"{!Xh&=Ycq5O0ri>6[`\$,sεaRoth'_8p[  ]ς͛7OKv1G8ܐB^ZuBvɎrh=jmǝqw5lJwp]km';_`#ڴv}>GlcpuٛAE'Ae/nsެk7/Z$וUlL#?ChgQ\j4PI'KvX}^Y]8Z+D5_Kn$[jERw ᰾݈:B'Xjuػ/f] ڛ_!)nG~]_>"WV5Ӓ F6i:۽ ?x ql"mj|N9bZhU ic懇/|T~pMuͯ@kGYue6}׏5Χ+5OpX8DL޶S)*5;Nz䟭LʹsZ){շ4~ 64@lH*6mXJn6VSF!~Y2_qv1' ݻwKvξY)JQ&_S+CJ{sL\#zr6Ϲ ޼`}h7ۯ>zFkcVv!h\MBY4.qZ5f]d 9Xw"u7yt2}M:tkRv(Dl0.KN@k,VSAl"N9.UeC<28(۷o"_-WקfVlKa.p~JF#ٛCFq*8R~̌^lY)- V j:+eյYQFrCγK:j guǞ5hVia1GRAZ ]T=0jP =|"?uY.f_5Nu:9t-_(+|?PvLu76lVO;N(猑$lh4;Ź :/OmwWkk lڠdbc`cќU+\.tZć_P'zni!8~7f[e3w]8^^zjbi?HxWњR;¬;@k\u[`t -F2*gɁݚMjmHss5Ϩ5(*H7mAfW!Y&k\gX]wcʘSfoC1wO=R~_D@@б]V1wUY BR(XPT,`,Wwֺbo`C]AEDXD@E: $!wf澛0" owg9gf=SΝsiWCxwNKSBвk3רb|;Q3p}6a-agZo/r&44[4q֥eVe1|ZӪma 6aNOl43+!b`ũĴڛ'Cؐ R?ѦOs~qcX] }`O{)EbG8* W WHceV) dhSai(Rܿ"ʳ|,Im}T bs^֑WrK p f|X--e/({yyyl컕 qbXO ?Smxּ֔_rvӆ=W\6|ԥG_T_5hp*]o$3Va%Z& Pdgx9mYV=-Ъ`CHӄ4,tc0݊K}c' bJ4~? hH,Q+RU+Us7G3gV4~4zv޷U] ɐԖnݏڎ5D/VW*?Y:qun|{`beۭ<\Qr]G|>nlWE'>ڻEqjÞv8UZ ۦ[ wܒCwO -OoҰ Or?&XhMa$#}y7HM/ @2W:(ӧ[(-Rl~,pm^~U\Lќu6%|~ujTF WIBcآȧ%TWg狯&%5ù5 xt$鷂$g19٤RRSS0㝗1-Ql|=z4ާ9*h(L䄦TfJUdrMY+~cEuaOL9-jb-VOw׫Ptd2+:0N_9Wp$)XHdҶ/"[ -8z$RTL A`Qo0IV.ԝ 7MvIS2"eȂѫIҗ/UQ~9ULl6ȍ?tw$erʕh)h4nXuRW/ZxZOoe/%/lmLL'o3׻1\@~>uE\/7ס.W eOhp:MS,IZ\~>jX/k~WJ(Jc &a+]p #F|!2OO*Qt[:\ )`NAqIr!Jh:^t&>RJ8%tƌL)7?.2۪$a; G?0Gsz c7pc9ֵY 3ԭ$UFIOaʰʣB!F3R؜pĠ"pk:Z7,.Df/op15JZ7n 'ND:5ozx}R<W/ 3f@]S*Z̭ڻČGl2N'UxRZ;bjiKPϕSirj1U\x2t7j`u+o*lVGw=337&,)$%!Z?ٴ&~8սo#L2Pܜa@TiaIx 4{rX}0EsNג$`O" m0gЂ,3_fm[%)25A! k>!6?n0NQ!\V]P=i @?I_W8~o 3stŬp/M6ݠ~-9qU <:8c٭ q agW7JjeXVt+> e&q%6$W1AYgZԬY ~ 5ҊSͪeH1:!]\ãA c $õx8-X+seVFwBee{gd2]P0vP>cK#]܃/NvL0W( tG3gDK38q>qxdn' X$9Yi,vU]E_mV%]g8닔H_rW,|g^A\qg,y+%!?z`pup>-Ei!MWXt~^Y-Z<}yotmN1޹s1F5L'?-:g&myvHGUܪpmKW^o&|{9Sָ6U#qAWr*w^E8|]v|VŧʯBT4?r&}).oجs|:njq &0<<ꂥE'`Z}P1[8x묈@"pf <*s]dغz./:t69viޭQyY FW^}z&<57⥀1d7|1Ѥg*饵!ONH`93ĵ{Ww}yْі?~ pn2/Viٱ-j2E:kk#N%uKSkҬD{qEطHtkq(W#]m}baeYmXS? hW b X_6_DgvuVdCv`"q5bLo]PC|jZ sfu6~ә/~V6Y7&}P4p/j7?>Y&PF= dʩ7U *G k|oӃ#NhԨv])\5~>lV`TR#n$#}5CѳX _v˭8j1s&}> έ{J˿y::Y:;0W H7H&?_K{{}ҫ@GnBӟvCxcp۝q_A;p{)>"g78y7`l˕|}akb,G j ^d e{pJ+,ϷSS XK[WeBL!W6I*г@_3nͱ[&hߢz=Ou?F>JEOmiՁ^ymR=t"Mɔ(Z ӗ0\p`s<+M~I_-]oio{r$W O?|^CcǾi"=)]$ 0E]|J|{gxyq[rZw?3`MQ @\(Ŧ5Mtnqm03`mIh;p-sDzh4h;;hf^96X6J, bd ٺ9/_B.#s\G9n£g]0=|)_ \sG'ۂ1Kțp0+|A8b晨_%k!&{ *s6Nxy d4NK]bƾCc{ݵ%23j^X ⎏x.ޝy:|?; êZ81VZ%` ~{g4,3_ 78ʔ mvû_,q6/m% 3=hcع $zJgc }{ Ô]-]X8Ũ\݇i ,k_OTNGvŤ:s[yq˅`rx~v?wT gx?\Z98-t4 nFJ7^{^9_\.s-qR IDATs>ם4GL0˶QrVds7jdgaŴa̱~ty0F=  zv|3ERL{c?C9Q~9K_ ; iFߠv6)?=t6.} }!Rߡi0`횵Ho.Lx=U+mQpx6g n<">Oq96qLHCJmtwrd㳷Ǽ,1W0cu>*,=CGbqWKG,9KGcVw|t,\*з;} rvӿ,`3VB(|Tl>_$ ZxɈuDҒp7fe$+McWzoCoic0ƶذ xϸ? cC#> w~MV|~͓퍩ïBC1ے.ئQkNƾ67YFq?Ryxqkb{`h.P%Gdcȋ1垸ﲧq4ʭ' 'E+=h|OK6fp?z|~'b{>3ץܸ:z<0c^C}guiX2^j1FLI#'` ]ᏳoŊ}/NWbsKGjH l8,dz"t7gF^hd_KQΧp`4c X> SnT8E~6Yu#HQ\)4]BD>E啞]uJSzj;JVZO9: K<=i:{n [g`ɩ~+>q`F\I{/G X W<{ ⓮WNFφ}Ñ`;=S} 66,o/WMU&q!VMq8=D iYs'F#]yݵ)=he*; PC}: kЩϟ]FKfn}Ѣig|sXɼ7I;>b?m:w퉿y{e[ދ(pCza'c.BU3Uk9MlQFQt0­rlxu-xbr >vh{,ŤZa=݁9:nBW!XsfT>Ak..ň/∽j"gzxeqlkؿ\;8^ +* /!f;āFyG uk$ScnWA7E4=k=+yve VKEAnP,]rSU%BtH4zLs}I 1w?rg=14ldr]80Yh$'A:;Fg6TCB3fwA'2-;tMoGOᴃ2Qp.%oy Dqal:?wWˆŽ{6n_nBpݶn%?[Zx1 OճJㄸ"W;J6_ у*qڜ䐆ժL16Ą۬T$qCa\q:izC3 >t9F,wåO%ણF]ݣV҇җ?n`i(Fn{ezȆGF4pүڶgo7UlxsbݭsJک&jѷMB{U(n\•ӟĩCX羇 8JvG*T&^($/?Uuf, Z5{[,H,=z3x >yəm#`+>; hM8=h u=wC~hAd2"76J鍁kAz?7wRўcjZ|xql`UyS*GV)gLƘC .qdՓ.}ص[uƓµo2^[K7AR?%6g`Ę/+FGwl^xioyaKѹ6ӟU)OH&9ѩ)) l {i\nɮs/[JAgnF&t$Șp,cPa]HMB@&r0jnuD7,:zm`R{ q{lzWim֋܄)7YR,qQlIDr & w6t̵gPYAUP_\1TUXRUFbEMV1XMqW<-˥neY1c:PuиVJhؖ uڎGMɪokp]ұo7SqًFR)VmM.==#&~~]osn^ݸHܞyz\bJqZX;D7J=ъm˭菿uB[W*u9i~.81Gf aЫPc1ΑAIŢ˭w[dh47wf]IOJizy{Yyqуy۸zcw(/Gc:&Y[k-'tF3m.pᵱ=@Q+X܅bWH&;vD`ڕPgC!9'h(_!HGŪu#Z 70NNVR[:I^IN& xQC#Ѥ* e˧"o~|t>KKśu#_Z[T/M_ukS.*$2Иh2W!0gt3h[>\3]HGSvSV፫@Ɖ1Q?\vӄZE!߃E_^t#><>?! P0G>S]t 3yt*ǜ ׸ \?h\nZ(CWtc07PJ;  H^z'9z\žyt U,hJm3T4AgD:ZpW^ĥ?iqkgO; }+<^cIuhjlo>f虦9ov06/\-X#ay5?}3:Ϊ/Qx:*}~!t7 9_dteYx}qG{w0 R*ۼwev 8Z@VVUj\lc<֣?@1572PQ:o\mJ:EkgG5'mD|&wt8/\M7cnfE? pB8꾷 kYgJ8LFY.5n-w^+HBmp{çgU9Tgjaԏl ?/bM:˙d77 xd=ͧ,͐c13QÄO@;K}Y|C7Jߎ r*N)m=VC`ۚ^e_IWLxYGE1yLuuNYzMqDX 훶Dv1D%+uۉ,Cf3]pTA 64MCԝehT"{ 1Wsv̒ ~J _KTNn)d/G9ճgpdJAV{pLkN|>'~ B,D]MQxaQ:)^%Vr:!]!鏅} ;?K5-/? wo͗F݆8?~6Cgx?p:z+V}^g~'Yܦh`*MCah4П"͜ `yZ:ݎE32X,,p?VR9ysg`hs*euUv; \СBcFDͽ hLNd1KR\"g[WiN5pes jn te#hHPd/+ƭ7฽4AxS:4v&:7|Ю?>Ǟ\/` CutáNn*.Tá>8{gyHL_졒H;$hq:._dq +Ŵ>@]#oݜo vrцTz=x?N?PgZ/vKSkù\ʾjAAqee ~P}N~KZu~:zs®v u;o^, Cnyg{EBez(匌7?" WzZ3̜SFc|ϰ9ǃ~3GYN+?{<=utxP ̅r79^P<.n?ghtS+f|іuǷ竪t7W3Nz ҈^Κq^4}O>UGc>Oc5r.x\g:}sJk!#?6+D]]c7{|%2Z˱ _tpȽd,s+9kQCGcL,N̼w6>33^SNȹ#i 9Rչ +AR7A {= -k\:܌wS&F`ٔ.*ȅpеbh8eArkSمb_?gn:>Eʕ+b _Ѭ$7%Ÿ!?pH~H ~qN<8N<i7`lZՑAN~渱o IDATOYqX.) ydjZ54IE˼"ꢬuH BI'ʳ z+EH ~(Z<+^<-ӊpR>/61Ihdũ?'.Já̿'}^.O'|j,V xG?S (Rxx5O w|TjVB&.O9Qs3W;jvת>vZvAE>|<'XM6<+^Yj0^\=ܕːj k_5g^6>5~Me ȩ]Ep} `K17]Y٨PW\K Sk"u4C e]po^mZ U8N/Y?-VtT[s;rep! 0pĪQ.{aڠMθϾNRf at0O4"x>pέ5j^fRӚc"֭BNJ:W K-!#.ҹZNUX<9 rŋE*jewt矌[Ksu]d;\ pQ_y9CpVC $KV,6 ȨS E+LtG)\ W]tgOU:,Y5hguX2G2q\-\DP'ޝi <0 |u?Ƥ/kdHM6Z\*<9zrnIN 3dιOEH=ZkTۖ\Jd\4Vz >6+҂x8?8\iǥt?< r]6jԨa̪V̝lܕU]ZWzrrLA29OɔNKY#&!vc}UěA>- 8RMC+Vȏˌ k=˰'rT57=u.(=vX|DX+:X0 WȉWmnҺ^07]oݵ6kTsOwPcϽY~) ^`%4k3/SUҤs͐SWHmv_g}RNtV #C8ijC 2̫U; vF+}~1W\ٗ<3|֎q"2qeVAfi?z'+@p̥X_[,w=o<|GGz-TO 2s򫈴_HhfK{CpJj5TPD? 76Y OxbiNp_ uvOÒZdr4܄r,IL xvZQV 4ţsLīIm:dajE`62qNF3x XuwJ7"1qNqmdXpg ;Ya5ms.7 e麸a5jf:c> b H.W.+!6 W"WX+U܎?sPJ7"sEi#/1ʜ sx"jq-6I4[ۤM}#$Tf~F&htsVAگkiG~_[M(y#a¥b<8}PosxY~{ b oSI CakClP"YC7%2ƚ5/T@i-T=!,JSH~Ygcc+#3.-[특W.um=IkRn?V~6|L'7 dFPv<:'^}s $K';jIin57sm948z2(a/rQ z{CQ/e=tZaL,`$ 0xY_ !8^>!\"Y%W,1<7)mxs1{vhS  &ަ] Da&\hDctoEh_I3Ac8Ei!DTy?%?Ǹ`L Fx.M}JÓ/KWY] gS2{@Yc$*9ӑ3yB6[Ja:KS\vи-Q-')_?jP1c$StigjȴI\ZOocWGF=-+vmj7r?n>D]6ST/ }#Az o:':mf64 [by0CX+0#4Z7h:H۩&9r*+$A ?(a>kjUY|3rbiP&I_bxF8ul54]ڴnaϿb:7hDj?7|sfY>ja apprR[`{>qtb~7&ڭw@/cZN 3" dw.WLW2*kB1"\X6gڶ˲iu 6EpC'K3`>/;Bcxʥ]='R"R\$+YSXN*ur$+. Wpӗ\-A,37% 椤n%$#Y'`Ize^pImDg'6K.\BI-)\5o6OzToSIv:b¤i~ A`_ǶmxH\ʼnWd^N Q8pǂgN-.&AS^У 0uqt3~uǕ[|dTw>n)W;?SL$+V_U噀C8AIzԟEW,_81mäJvm۶+2 8*XhZ `0N( ͨF"9d!2v L:"C f mQ4gg@Wo0Uc93Usk?q 0-M݄VX;vÚLOS"qBH>.[$.Ӑh֟0xi|5BFzGjt@|s6]-I)XJ>bŴ [|qC8ZIҳdeR~6ĤݜDc7x=zgbx^~ҵk$}B&̝S^o|mu!/)޸m7">TBRV;:@LnDQΈ\i1᪐VngVC| ^%mYV$V]C;<>  +j{8zA÷0h;撵qJ~'jY?g\!$}R~Efcܕx&赫>i׾}{qEz2zV\M=cKoC_e"=|%/Au8#֧з>iUZ: ^83 .^FrhH#׆M~fc[Ek۰E=_ hŝѫcj ݩϑd$;[DH3Oa=)Aa ELj p׋=}KJ`J=K QSrVcX]pXSfX]0wtDۘ>3voQ Npunڦgm9E::Z4S4*r yW-dg+wɸulX%fYP*/u i_=SߠJ(瑽l-VFq'<@fs)& o4Dcn 4J)h8_<CQ_`.% VRz^Rz؊1M9'uR(dlHVK#\B4(OrqwqUVxcO\ w>ҧ󑗗~lڵ9իW㦛nrm 37uʼk͠1<ޤ1Z=>k|1n>zSt뤭a<1SIAK?0 Or P{91]c95u}2jV@/öB F#|r|i(BzO᳟ϭ0NQ$һo!v޹|+dtI RJeJRUvJy0jA 嗍Pûa_BSvtaƟ [B 4 C3 c \:p q%S2y<;QZmQm/Af7C:P>奸_r,WBي6Ͷ_ϲ [Ƴ\nD/g!Fna?t麟]dx颲p Y ~YAt>ϢbGwׯ@µ^·t"2e0%(*2I !m+++Q '!Q?2 %:sX2*S1+3ڄ7MwI*+qɨ;aUQߣ!]FlP.6;FV37!U33d(.]GB&;RV8#jj+Y1]?1'ӏaŇ[j%} *x.f㛢k#9Җ=C6Cpk ?5k#}Yiͅk_Zdsl|uB42@ݱEPQlz5N+rjD\4;vCt`3~E ؕ .;3_Pɰq"7ק0mCոy;V^/)>I5d(n8m27$cx"7: U&*+y9 Fy^g һ-SX;g0R0T+.&&,I_~PA`A0BV% aX-q:vZ37f}onWSOM7߀*iF$'RuCQVOt }[ KT L2h]*N3-O&vSՃ8~!|;']R߇gK\?~ͰO_o 3FMmqcUE5]vIj ab46a4~ hu Z1fa[nŗ)֘=o!2os 7<:_ [?b ߶T_2ϱӊxW0mY9O ȹgG-N0A_0@8i' bL3EK'3UP DqEn'l|Ρ1awRb|ag O_lj NB,`7{%/W`l}X/F-O'0 HN졀&1,跭npw iTīo܏#ލX?6R 2bn\2!2VD5ttORG1=ʋz8*tgJۧʰ\X<}n!Bj8o I%%J棖\|x->99 ?uğCʖ۽ xLFJXF{OWJ>DL**[ Ensbu0 ggrYA1|(j 2[yUlu1S_Nde,ϗ0A\i8<\@ 7ÌA(1)P\7("K1<y3EFcY.mɥcCKٶIA,-1Ԝ;qnx<Ej.IoLoC]Ve{2^v8O+Pd$YI>ܱصc<.~s1뇿28;g:~O-'|zܻĚĺ-ɰLӰ8UXÞ&p\4t_ ̘%(|b3Z8c*Y.ϲ¶Uau8a :FLC6E I/)On"SvEz| 7:w6mݻ36k0 鉌DP?Еs(w߰UPq9Za6|QZ(y)֑Ŝ2DC܌o' d*` [:W`#Gf Bs@rtZll> ˵bw4Wr ƈ0 ʅR; CzC[+yZ}4m%7f bLKrrlUdCPnK<&[?0> 1Wd>5 `XU|va ,*n"ԨQ}xd<okR2qۣрn!ucnWSWn7[pɉx6893Ne= v"l I+&!]epgOVqRF(~`߬8vcȃ_;5%)[Ri4]i ^áN|qlk6[fdSv=5ycҚd +MJ22lh,0-Bg$V@JmW33FndԿ9J7C2a ;LOT~ӭI-#h]ڸGF͚]9wa[(N慇zP$EsTVs6[za+bO-m\/E"4H7_! +si_V[Ype3ە ھC7T:[E$x t\$6?n8jqӭ.qbZM3"cQQ_ŗZxJ%WllrF>'{yY?Ncjm`:zF4*g]}R*`l]74&'*`$~8atg XNE>JLvi8K$ Cyi-кvV-judf0~¯ WE.;ՊaP _tu+4hwTC=Ja:yYYPf/OK |Ec[8Aox8HAVKadHt<Kϝ=ɾ5QȘ1Y=yۄ5ݿxߊA%o.[.؞4D3X(q(,wqͥ%+Ak Fq@4qI o9ްaC=5.t0Xq(4M ՜84EKgj"‹ѓTۺY24!;zj*М*HpnbXHs+Κ(?RiGUfiF_(-(* 84.*7 $S[ԛ[ I&fj%fAK;"l7IߦG*i6h'ߔ\n>h!mFe&ӛo{}Cu,u+6Ϻ,8,Әe$ -.,\@ҪuU/J"|w?QRwN_g.OQiOqOagT/-駟x'I 8U [4QvIoVLy{:V j-C0)[f E93\e86ZhLȜ}X<ؠ d]S=$Ȋl_3m7-]bt!a;lW(<5._ؔ{s\H+(9[:Jmby8SKK-px>JcTJef^K&/J`ݿ_V l b5h=&L5 %r!-FxpEҒNIm?A7 OXlڴ).tn ѭPz֭7 ϿÿcO$]9R%*ǟ{* [k"@uaf. heXz "(;g#@\." 7#ɐY0X& (u^9..:pV&Û> OnQ`E#a c 21^knq dh3(r_W\~UeAlARj07} wV8$[K)c,I_lXF]4$_W_u O7>3]l EM is+GƩ 2=ڞP Pi`AG%-?hb3*w 43jFƱe`' eZ20is26_/ʇ>0x(>Xɒv H9;<%+[ڟړkV29JlfMmV \~WDA|r١+wFig SLZk^I^v lz4k%Yqe@w 8KXn*_"f?~{/J[ -; ʧB;@OcQwi*ƎbAEޱnXƈ]{EE "6PQ{oא893흻gJD"Q>Jyzdҽ'##ٓ;|p}&id"#Hne.?8w$'ekieiЃL~-HH*+sT=2#9S%ZD6+ ylʙ%r Eae5)xµW=}fL3O;3,a w/C 9qlOl4cm6ⲭl\1Ov?b)#\U"Wq5U#EɗhȡgD8[ހL4M*wa (pu7!`D8>aξڻ;a„qyw_H !\R~`yMYRr%h "#? ]G;S"曇AG6( `SK1lYXpuy˭;uDԌڹda|39/?d%3\d,߼2^i4r~7an wQh߾w;/:pn 3afFY6>VnF{bg,$h&[{g0Y[G_u~YmSlsLNRʄWq|?RzNV.݇ 4pt7"P6vw[,mz>_hLOC߾}/zu9w,\NC_.7kj3i!BhˋNLљR,-g6{/_7dr4qumgu[>paVn="#l~+ҲW gܟdi\[S< ɽg)R@Ybn!hU2J?fZ>y bb*ɈD̏#(l83CAG ӻ4nK;NgG^,1$ CsRC*gUrW!=BJL~]z)n8F`%Wңof\)_D[Se;M_Л[Ka9'j'u\36|3p甛 B+]xRl@IM 1WjXc ӣFW\1 4(|óv uBufmӼc ⬡ÕaЀQoyXaKyyO׍\?U!n%fXBa?.`,_mѹ-^~w ?w +Lߞ' y[lC7/չ͆?58kD̀=(Fء;| \I<פtMT@`e ]tie=ÍwwdQy֯Kg]ٺl ݣ^U=%~HۗU!ARd[Ӗխx/O}-feT|h`uN! 8lCG0aL~Df1!Cd MHe8 qQX)s~8 i }ɉnJV,l;Ya_+OJ &M~F?n{gKw1LՅFkpFHAWK93Lxl~/s~\pz9smmw*7~kGzSoBlY"m(AOI|3:#ɖ,/a5iQ*<[ne2R|\<Ξy<|хf@63dGF|9.M6Kqmrҭ1O2Y;YE3fάvV,>ޱT![,Î#!ú~aU;ǜҦYGk}-"l^e&yy wFelnSdR"Sw]bJpvLr̭RK{`5x' '}t\pveV|<3w>e˃:uL>߳™oAFRnvU+s-Cyp,5 ߮. ٟuV9S~ =7yagZuD0S m`6IOLqҽPjnVK0Oxmq~.qF=F8`ϛСCm:Y;@Læ[lvfyHx0FeP6͚4_u>ҫeXQ(sFdq>>7) orLc/A-eᄓNeGw@~;k+cG]灶*(M:UPݤq}MWq]jaY^+Z=8jb9'K}ey+RxG¦[n~<ǰܦK愒8 L6R*r װ]K^zfnY'leߒ.'<*S(-|ZN'ċ}0puf9:~CN׹TZdT90VS?7?!!1ДizK@\6b'R4O,U^9;~م_ ]?@m=7a- QXF<~SMY h`.˺yvY,f7/Q~5R[""9gR܊d6>Mu雗|:\z3ҵJ=᫯ 'O?ag 2Kh׮]ao~Qpu͌Zke-a襌 {qJ"Pe}NI[Hy\#ͳk q\T}/?2!upΙwȎcq٫,]E|?%.H'Ʋ!-oG yǯQ׏N@5vX`ÎۆͶ;rΘ=:J̚bMV68]emLRڹeL8eybWʲRMFЬV"LJ i&* GMGyP櫙' :t@gLjT9YP42?PI.MO㪧Hoܖ8tJ/*o7 ~m~w)t5*a ,H~W&q rlc}d'?AXzqV,'΋ӧ0/Ffhm 77l|V.w v":' ^;L>Q RƔh)}A$R?O:2̋<6"xkko 묳qC ~'U&*]׸c©wqۢOzi54dB3ctnZ}ri)'+.2VD:x8 8}:~ mnuCR&s ЫW0N;mp%쇟M9ZכleəʳJj)DcʲgFJ33}ZF?z;N8dHURR#'WrA&>4د&/O"N,÷rK83N:]pL3>[ocdž/\~喿袋 70a%̦ϚmEqQq;E_-27lky4c  [^'t^}6"i|O$l-{Y4Kɰ*F"hi4|p~p5K(+_O4ܺaƌ ssw<0ˌ: 4p s-GXa,+#S/Z(~&C݌%]JohBJ׿袋o|/; <-y W9xyǏC׈|,}L|gX A#d,Jiɧ{vyH:Gwel|2ֹ!doǏs!a[%^ ۽)T-K,e01c9|_ͲK,Τ^KI|a}> >H>WuW? ud/sr65Nj a K/.yධ[yL'۠4lgwIĺ|g :/] uױw*!72vϚ;\gqt}n%b(ө˷Ē [_~fXҚb,6%ZrzpO y2ᲁzդ9ZI-cZt6"J>XMA_SPZt?~YwٍXNr8 [>;SOÄ :λ2wmkXbꪫTV¼azuNGi$G^S~#xQ|U!_ O.yQXj{m)=d.h~ |̗9?83dU6X!|9&աo{4t_{xBNx‡.K-.TXov ~nb?6qXrqr*/|&=~<=땯O=zRe>|*ր1|_䰡[_ lho2Ʈޜ$W?yzX\F\!g(D.9FMZm#g:'7Hu .5Htr6tQ_:evWxm<3G}tx7?ogECj{T]v%,2}!1VTgW>ipvX&?WY;-;$So¹]VgmL)B-.q)$K2M}|x!dI IDATvp5淤фXE7H4:0ݯ= q0S\qӞqJ;UP]{ҦF(Ca)82i\唇r ˥՗؏o_nfѓƥ2bg-_ux xv0{mrqé=ISqrWTV~,\\X^2qZkz*sEXeC s.0{AY+}yh)_x{T \.5:g|~sav~f6es{ _GJpԱg/}]g9C.ΐ- +_rk2~i8/N{/~p׸Oqpi)r=t*r5Йnɱ:Z> s=Dž7ޘ~6q$3d/eZhСcF9,)X\xsϚ<:ymaL[]Ӻn?;{2k!fwiھ~8@t o%^Kvs.Y_\.u(nN94EZ:oÝc|xlQM{n̏IqJqijjWZ!'pB馛I'6x2)fTo-7xĈs _ZJ gKHkɶELWn Ea# _y?law } pe;KX&>S?֛M6fǨ*UZג)+k+ay',|󇎫/f9td/2-!CI5F&;;0Ia˅w#lVplL?/NG͏~L~!gy>=< Y_MhC'OlD2L2|DQ;G" bIAhSњc6y㏇Gy( }b6z::t58[v18 3߇>~_}]x?27aBϵ{۴ݳ/᚛TW-O}E˗8]a|>m y#-zԨQt~FiFqbh!戥V[-t~M]:'|?xۥ'^8{ XT,UQ;bZM)[kE#ȑ#{#C¨L4W=?,N?~N[ >W_O^N_؇$,fu0{9h$kme͍%5 )WIR:%WYf?-޸qx{~Oro\y33lw-[_{;o*9fi K-]}78J]-zjm%C:ͤIWǿ>09e#%#f%{,$vZ\7;wnY"mkA'ĵ}|_N.2?CEej} z"<ԉd|0?@RV?-km`чAD4h?/<;!n_LpQ*+]\Sט{_8þk$v0}! ^Us<~ý{1 eΦ3͚Z >cL9uVo;֎c qm".>6\y`={{±{ik4!aFD!<\uyH`u .|.|4}ABXkH3OWνʥXsQic}kI45xyg!bs~ph~8{뭷lo1;vnain0`@;l'?ObЇki.[OA1ㄸǟk˥NV{s~=k[hB @pII㣁r8"]#h!C'&=Nsϝ!>Ĺ'IØj|˦]n&j;\*1>OB#5kUR,wƗQڱrEU||s?7ˇՃ HG5(߼p5`E5`kPIcTk4n׊˵U=ҽl1dXk9(v~ixOo>C쥗^jSVtևwe֝ GzڱtɆ!%|=zO>YkѐD(4_ h01LC겎@\|⁠AC*ŃJeMiI~O~3+4 J|ǽ;3Өaz +x@?裏bIf=S@ 1={s 4(\}? {WvF!Ezp}چ^l 羈Imhp;!nؗȽ|0\OdxK>7e | a K^ekBUvzìRWBfM=5Ɖq8*'C9\QbTͯ8 K+D4v4]X_*O74 3cs9mkq6+EYĦDs1QNYL-{dkjM?07f| K4Vǟ~oBj8_ew@<`0D;!nj=RI>QWIƾi)xrȧ.db >f̘.aÆIG\&W8WiLgy6Ԛ}ٍc9fN9kMvQNa3}]y"U Kg5ůԪ<ןJԟǟ%mOs?7+!V48 8tK?@`AxvsOݳ5\H yErq҉\pOr*O*E]cG0X6QfÈ#SO=^{m;iԩc vuo&2;Ս)c=Z6b3qG©u)_:4.TGqG)p}9!n yY'N?ߖA@ PS\5狠K[IOPܱ@<4]o-G"BXFyiCʓ_$.SW_ʗOyRřnoLi=q~Z_*Ex5]t zmjX-,OG}4v,,Q'2çrYN8L&Or_iqp,S):*{#rh|׽P-bs ]gͥ)j5f̣q hKU95n+9G.W;tȋ%#J2inݺٴi3}E.J~eЋv]w mZroMz2&!W^pQGW^y%6Jm%REdk%YG`DUApG6Djnl-bXweyq8k8Z,￿m~G[ޙfCd9x֥6k,- ź?>7.1e3?p;n02}7vfkd!E픎<V^/DzvGpYCܜ t]#в׵# RU;ej*Ƒ& גĊ@3C[:cٸ=qX2iZ7)_ev~ KavfC0OCW]uU۽9 3kċs'L[x'GhCk%iig0HRTZzrB\+Z.L8!s#0N#a]o@tC4R{>,vif)$.-*[n% O<@O4Aȑ#BV[iӣG)ӬEfwkvEK/d%Xˆ1m⩍.[4;NVPN _x-ҥ4Ee&ݕ^-: _t|/m\u7vj|5R}ʐG9mCƱBj7tS[3gq ˥2=b?3Wl7xAYc̚cPO81p$}ٳ_tEMjX<ZOpx@}8!!w8N vN5}RjE2rȎ{"j1^ǎk,6u9>iV2b 1.ҡ4q{Ұd,fi30Cx뭷ln}e2/ l5syB.%i]wZ'-wZ7N[w&CfҡȦ)Oe52v}lBf<\r%CN#fs-Å^y#lկ_+1.Ž QI}ts=׬Cvj;.ؒW}jO7@s!t=@D`-#8@K" r:D(!.|ʧyiyV]i~CL9_vj>;XK2TcV8͇87x2u_!ȐWjdzX3ܧO'uXYWݣGnsToO#8#В8!nIt]#8E b"4^TJUOӋOl:N;x#ŧz+r!Skq0{3$ 9VuypGh)j5n]#8@Caɍ:JFirʓqzV|T\>e9_,2lYloְnfSi]i O?t7n\`/Gam60ӓ#a7DM8iYg8Dr,}%)^XNaaⴅsG=pGڇ%]mAڬupGh*zyG#k[8 I+E:*VKlK{nFz\pA[[7o8ߗ|B>avfC+\dZh!;7]v5u>⶧ UVq`gm3Z V:N%dV[/}V_YwR6};p ppG#X(MEU=/*#&Old9B؍֋آ:tgyf8묳,3;hW)ͳ>յ ;/MLx#BL?ȂB!TaOq[f7m.Vy} zkJi;S4jԨpWZkAvwGp'͍sG#P^G\08/KbqYɑLBz}#F,8/@0!X]!¬픝˺ޥZ +,d|}6-v$ң>XS{=aÆeˆ~l5sOQJe!ؚdښbH1|3}1f_W>m":th'.n9bpMnH?oͬݒ>Gp⩉84LQcW#0qcqX"djSᣏ>2%酼" 9$ !,bĽfd]/s嗷u Iv, ĢGNU[6Ӷ'1cqEW%`Ϯp‹5X{)QӏyЅgŲd#FݲLi +;#t%XӶL9)!pGh | qK:VLׄ< D0M6l3Cj+7U+V ElCjT=qyUIX>F[aHWa\felZ/$B娇kbE齔0aVHSyV3b.ZD>g}6Db渮PI;rB\ !so| {G)H$%?Gq~ ^x~Ö[nYx`?~i98SpOCm=3řn9,X~O=LEI4iR8¢.j$Z:wZ Ʋ[[ ]~_'~yGp@J`㊈a-S}i"}e]fG qa s1GW^Y8 ԗx5JyBo=SO=eV>}e.$p믿MJ:[bq2$ 8#;5fr<|ZzEjkoOز7rOnᆰ^{dź4ٛmYٴ=8Y8#8S'SdpֈXt0S͚ UwyMxNYŁz+̎>XXufͰ 1uXJ QM<ݲ# K.$2;~Q #NVXq6Cv(^=.Lfn\\O\ϻvj;hl\}}0a8#4NDW8@[@25WTA`2= b}.&O*^_ʏWe)a+b>6]ٽq͹ڔhp${vwɪZ?W(G99ɧ {xGi,l9(&6͒c>g27HV6aGpG8!n*^p6@)Je*VE,L!>(ZW"}ZrXV7xc|!ƍ 'p)^r%mGm;t:(F V w0']SVckO9)oyvءL)_ɟ}yu%/byqݫ[o=;{R^H1GB VL߿Xml5YsGp6n!ڈ{}#AbъE݅4qݲ5$RV*ՑH*_)]ڎXg-oٵ9Ǥ82N=+~g~6=]kX'|: ͬZ&.r1Nj|qMa#<#8@K#v#DR\t#ȑ#,|Ӊȩ MiOd LƊL !IOK,?#8@K#v#RSٍ!ZFR  K*%B\-+*4VŇ!XW*M^xᅧP F8# fT-$7uXq8=b[̙\ O߮{4=kf-9׈;GpG`j jM GpZUIJR*C%G/lImFaw`>ONʗ-~ô(gb%&:\ӫF|`k.XrKq;GpG`j ⩁8@+D(vi<+ 3m+{jmCi&=S#-0OiXD{vG,i1mej1ӥ)'qjM.ҩJ>3aDsv1nݺY XݻL,c1±GF+鎀#8@s!t=#d]~fرcX{ؗ^z)t5,bf|Wm/NYDb3!M-+.Sߜiq]N=Ei?KUx@vN$1?#|*P>WMdS?." f]1}:dV_}0~χAVXQC2ǝ#8#05pB<5P:Gh%`ŻrlqaÆΝ;I@9&h]w .`HX뮰馛>㋜OMc8L.N#–IeT ǩE?W\W:f^:bcG9qjt:$'>qYSK>}Y|ɏBtpm\Nʰyռ(:GpSFpG`D &N2 V[m}QXZ"+&H> xMȽz衤1cG<'-vq>aIZ*Cy%K\2J.N*'8o7)VxK^._鱯vJ#̔nH0 7|cߵH/e!q:k zinӧo=o9#8S'SepV@L 7{w8cll䈵} }f\Xh"#bG8v"sIFb=\GaM<+W=Ԟ4?NOX9ԩ:XVJIUb Y=|O|p_cj5끙E+Νfm1z> E[lapG8!({#Dh*=&lYKo눯ХK;g":eOP>iq\au$..TeچLp\,4iv>sA q|V٢Oi>?,*dIe~hS)]tѰJ+tz6d#C䙴ۋ#8#8!ni]#86z衇.R>V 0'mF\_~!Ҙ1c³>XzqǕi6I!(#c.So )ol&MUqJ JR!Ľz2Bꪫ&tMq^qX!~}`:]fgJ<|=(ٳmAfZ8B]k]pGh,NsG #cvm7-YCGvۙu '$g}v85\3$pfY^M{wkf;w5.O?Ԏ^b;GpG`j"xju9#0 #$֋2RK[-l:<^}Ձ3!X!J8"pq݊u49HQF966w-/x`WcUɒۥKeds=g$1DjXPq}Je!Xj48r||BR /XV[ isiyv(SL:{GӪZqGpZ'-uG5#ti<b(~*iH;LCZۡCEDbYO,GyE. CLf7SF !klZuVYʀ/"pcUFS3MS\> X^Tx ŁV!C֋Cܗ\rI۝2ʹ̱JzQB:ӥ-4hY7|s'/ndwGpZ'-tG" "ȦHBV?u.DE4N=zem^jG6c=Ju|%bT<A~PB83?c\\V(Mҫ-Hkln+WWS;4WS!bcCXJNu'rL#F.!L׬CشklС6]]=P,j#8#R8!n)d]#8X W_}SOY-r)q4vma63, V[ٱK`鄕p*Ӷ4,X2@!슌Ř4uQk)C6ӟ6Iora _Bg `zV^yt ,d/!"}iq۷]G.Wi;#8-Z8#j41UXYGbxAF0Pl"yJ#]ĮH"lg*/ /<}DR׫WFeSpVG}yXf~?U$5.tL3Gَ;FX+osvV5g3L3.]wx㍍@d8n GpGpB܂jGpZ#X !-Ç/ i1C=lZ,$_ޜIKt:#2E:2бSVevzY$;#ZTVTK.c\(rK+~xL@U_rуEBy%%jMFRsۣ+/"p:ˑZgu['|Mæ~sb#^Bp]AC?MSwGpZ'-tG" "Yd])!p Xyt4j eXkuq*էyN"_XNu#"^:S\OeciX2 Y gZ4G!I'%ӝ6~#:pē&M28*i*}5xL:,tc \[. Gp@ qs:Gh@@8e*18581xu׵LѭD8\4"YyqX4`,Xo̊)E6ɑԶJe/Mb]nB,p3|Xp!ODR9*Ox-r"\NX=!_ɨ4WEiJz''/|,ݐT,EfQʔvܜs c4Vc; fG*1yf=ijlJ4SG>.i#pGhi4®pV+L3dLrY7SّOsnkks~;O=Oqv@ ĵv"CtҺ|-6+mY󝳩`1{ X9pwx3_i]q;p*GZ~"]8#4_7uGh;0%gL=zm:^V"o~m f2Sg#u#)GXQ5rSAb)UjN~>PO}±&c'|Au=쾍ewvuW[^ 7J5T;#8ME qS#vpĈO>1Rz;S1{7XA nM.,XJ,"E"J:bJR٢tnV6(q2FtIw\X(L9H0ӭr߆A47S_[sGp"#Q2.uص݆lavkZL$+/_!=!:⭷:L<>EP5E$JmStT!o֭M#g*9V= 5pGpB :#0-"tY6zgW_}eψ#9+\.]חzQN{nY{ܸq$+Q%PR=Ec9u&EJc.fWUW*ӐԯzSYWzr6I%W_N8[O̺bV^6:fuVvO:wz6?~Q֭6Wˋ<8#4'ME;#Fp$3ΰݥ!h^xmiФb\DJ.&}q|.i;+m[ZZK7hOrҫJO}&s",2EVܟ)2ꉤ/ ҟ}h/yL!g}׮]èQr>i$AY:r7*c5 .XܣG;븨ͤ]=pGh.7pV$k߁hD7Ʊ>9 Nqce_,kBp"Adc8򫹢z +B{ :LJp?>|SA&Sٸ.e%/,7tYvf⥖Z*tv/xؕ)&eTj֦vmaV {WXs5*SmqpGhn77pVk9{kc%4HSmqX :$MeHI[zjj/2EeԩSÇ}L]gU>V_}JEHOۖƧ"REb9N8,X.K0TgQ]a]bfJ+(CO=T[̺sڮ0$pGhI|SDu;#JxG2 IDATkapu1}W` ]\x*LiJYII?uXyWW*W2X= f﮷ʥq_a2wpj|Q=iZ/)˴g,J:4`iYwepg? .C )סz:-8#8-U;#Z`7o6Wb,kJ?pqٱ餎IGc:tYIZ\6+,="J480Ӡnjc/;.:քKFZd(ǴlooVb8蠃l'jkՃ;GpGpB\HGpoql'ݚs9mm-BفߞX4 =K/DDD HHR<驔%c+}`z3C1Xƾc=@|A^0q/.4||JGD,STʤrȒIe6QGey{%[2q٢pQQ^mvm V>})Jz4GpGpB\HGpu(;cĪ1c 1n=M8IqI&cTA$xIZRF:s)뭷ψ䫾X¬~avQXG-aLgG=QՕ2'ilE>ɤuq,׆nou@9x*#8#0UM*^#8 Ĉ?W^fU9rdsj, yk cHd֤4-Bb8 iEiK[oUWsi%Jy睶gϞvNsyc%~!lMXve 7`~'kpwX+l` B޽E/>ES]F‚Mΰs5=96*vGZϚp8CV9oX4J2Jۯ6laM6 E1ckr%#8#F;#СClͬ(k19 A%,쨌_uE"˜`0UJڧx%?CXdmdz:yChGaț0aY!X}qOڨQ_W3 ĭi?MXk;w<' i'n3 Y4`>39jbsGp~ {#LĄ&3Ӑ&6ý[>=z?,ȲYSNl3.u9&S^O.-jT}=t뭷t^g%Wnh^s5 ᪵ʗJ>mk&V]c{e]fŬ/w3=Xz)w,lեK[K[ɑp'~RkoN5bs0^@tTGpB qs!zGhĤ0FiYg>-Bkeg} =JTxԪKm|5 . aT2£Dȋl2PJL'K.NK@!oLb-l 5gGCp5ǬX9wXMq]we/X'Ҵ5k9#8S'Sqpi80nv0ڙXCoF 4d s,]}ٟmd?)%_++)>V{<-.MېƑډՕLiu|Qf'K&ʇa7yq/}hvgWp*D_W^1B_d ք33`}5Yvfs,\NѬƚO W_}YRDz>ӏ#8@K %Pu#RRERYf#|ucE&c66u:dtH$Sn(iAqL]ھ8(6zK.i~^Yh9Ή V\q2Iú#zeMB"|6ȦCyOͤ3c@Ve8vxpGh~O͡u8@CMv a!Zɓ'5\跉V6kCR>;:bc-dn߆k6z衇3<<@r+]icU11JxNgz7S9vccFh Ս7ئs^3Dro|?BX^ۈ,LW;X~m#лg&bes֜Q 1f7ky gpM)i#7tjESgeA\\G`D {2NPLJdt\LNc"a5qj|%3<3pd2PzL8OUK>Qd-=Aٱͧ j>V<$57 XGp,2iq3Ox1zo՘c +fʻ<)k|!ː^q23L g35xh6['[t^΢>x#7=/}|t#Ԅ@%Gz5'_*r!uzj:,V(M'?M'n\*x tAF y;,1 ӧ;Ì,,~%>$]Jz^ǸV|,X}9j N9pWZ_!l tw6²>xSKo=⋶m~yB'$z!^xay33nGpF qK#Gh%D5WDOyLA Yg.*9JbZrO2cI?CqWKfޮZ㏷cZԲ)RN~!X|ml}qb<%X';IcǢw+0&MX_wZa͇^$=ƘiLfS1t\ϴiϬf5kKLvpGhN7'pVTRLZb§2&VIϣ^ӄ8305ěnNӬ}e 0 }Fͮѐ\}Y]cTq*']r'n*8NJݷo_[S 9VL#g8ӥ!L3M+A)L̔hNYsdS!g6ڒvsGpN^#8(KIӉC qGj@|Mtw=7|[n:6dM|eڝ:u -YR!L}׍1-boaVӕW^ʲnݝMTZGPZCɣI48ߑ}\ک2Z_ qiZk%L,Ӧy1|m;#8S'SmpVH|Kx&Yb+,X4^\-GEL}fw'x"̦^l;v$c9e(blӇ^z@>ᣏ>2%E󗱒2}Bu 8J+J'MQfiBpqH`s,6%\S]'m&͝#8#8!nnD]#8HL0 XM9qʤiiJ,d/L]mnSeŪNW\qEQGe?A1c6cB^@2at6YO%Zꓡ?|O3#0Xc'H+ʏe=8#N[U8@+F>bR_~QY{O>٦YHcETJKuC2}]}v[om 1͙i윌J+}g^8;&{[f\ÚiFc)6l f5:ca![ ϴV?q8\Cъ"j|6$7n\֭ i[lEE=ʨ֮jy*#8#9́pZ1Xzq k#9]xbi6lcĤwћj%$ErJlj@q.aE gqFUd˙Q@rQRk3գP6΃rݟwyᦛn ac)ׯm XaS\UX١y=0Hk/ne m[f4Sl/oH^,޺O>㎳\~Ϸl&Gy}Huq嫼@Sc|:񂎽8e a;G>pB<}~kG2 pfmfӓO<)jb8UR> !vPf/G 5L f?଺;r?TƇ`Z:HUW]&LN?t#A4x,W.S7lD.L#-&׿(@c _ #P V OwGh"2 1eiC u8Zzx_#Χ,d9DXfZ]tQСCmc=6lvLzЩd:I]Z 70;֎wbl5:~]TFޙZUx~կA#}fFj D)F QEQE4iE4 IE/)Am"+rw{9{}k}mYSm WI#STi>ϑQYfV++ߧL:h$ PF%  H0g>ŪUsiNAy s1jˈdo]ra7>LDܱ#mݖoctC8N0!YfWٳg8 ͚bFb_3 sQO1^jxŒ:J6܌_qi*9G.3qѶ(_o$O߰$  HT,' H`@,EKh1cƌ47LG8M8O¼`k 7(k7lؐFY#뮻cǎM1]E⾒ϔp1?0N !9f .HX1=F#$  H` p @|*Im$6Bb ݾꫯNG;1#D/]faÆ5moKg#Vϟ_vi5kצ)bC/:mڴ$\ Ȅ}Dic4+V؎:ɇA?y4]\yQ6lmόTik3w%QL~9u3H5{媼@ cB t q<**PkZ a>íQF)fTrҤIiZŘ1cRlPfW.D1Bᙵ2Ey}M-ی>`uQY^tEEysx N^6>F VBο_|6".vfM3F1jh&S^xa1gΜh+eQY ;} ?p?y~µ,^! =J (C $ Z%d ,(oMS;Xs1Byi4]"[lYZh3 s m墲B^tMI _{ť^Zl,B,b|bUvj#dy.ԟ91`46? 0}VnbK/z(^{g}vıN%+S;bћ3 u:\9lE|5?U-hQR$ ^K@8ꪫ]6b~viF0"ޘRB0"ǏO"!b$#vbFC9{wɒ%ԩS\BA.BpO:? yʕ[oU,_Ͱ(3b97|ŋpf0:f_~)Fvh_\{ ,'AMs"POKWIhE2E\H Q]D#3F&Uc3#̌fSG"uuVjS9 /Pl(d,FKF܈ !xyr;/;D1\i/k'N+,ٞ7o^h?D#ʦ4K.$g42m~|ت> 1fX' H@wSh!G[M/;Z_2a#¹je"o=A+E #_{I!ieE[_QG]l`Ŕo8zL0B~%h4C=@GYa\6ϛUzmA3=;I'G7NHdN;%s61,)0ar%TR$9ăk H@ $/qy<}7X;=l5}4ec[5k61 1͹ȬK.5]vY^o`((1Y|"(Q8!d-Qݯ9f壏>E>ld9#G}F6F'L1bܼMy"鹟۰$  HU!nYJC8B! (0ՆhKj f 7'HS/NUGhyوW^IӣG;ikʹHo)SQU6bp-m9ceVc9{0Ŝ]h8y1δ΍fwiF~Q57pC:5ܱ;6ˮz^jߗ#ĵ&M@A<@Z#=|oܸ1mLl97G"^)_>y<yG.]4mŦ\tPZ[8%r!G/;1Cwygڽ9lWjKKV 7cիW'F[s *Zm`$mWCJ@o ({K\ăy[ #@bD/T3*FWhr>#<ʖ㞳tY'ꫯ#>z={v:VpFv[fs):4oaÆ$9Wno+L 􃀂,*A@5ă!E H@ PKڈAUn_.++qO;2/Wʖf̘-e&]K:~h…)h:\svN~[[؍uܗs:Y.t$ ߠn$TP?p#>j.3g,=⬳JӁlk׮M\ GIDAT#}-Dt(<_)D&S\RZiyfͰ~>ڂJv+Tq$ VP6%  t\Z%qicR;I;N?3i8 ̦[+WLk8btqxݺu|c6ۚ5kVtqR->ꨗ*6-G=䭖?6F'=yٌt} H@@3 lLcڒ$ !P*zxi$_k-vXdɒ,8㌴pmܸqřgYX"zv-L_"SsPBE$  H@ ne됀$*&T-ݭ$V[v{?3˚`W?__,[8ꨣǧ#a6u]Qu > Ի|_.hZ|<܌r,>oX$N vҶ. H@@JM"~ypėRRjqԉfc̘1+;wn:鮻*-ZT|gIs1LW*3זrzN~y/J툸 a\ئ|NFy}y\^9vJ@N@Awv$Ujh.T"?iUi{noEV+?rk)͛edxw=3O,wy*h/y8#.])=zv6#.G<-vy K@E@A,ڑ$%BDw<~oN^6땭ZuVxѵ>">H+(~jQ.J<e}TZzװ$  HYeY$# H@$  H@E@AQJ@$  H@@(ER;$  H@$QlC5|l-$  H@.%0(1$D 5% t0q?<.f ܦ 9 $x;]=qYB$  H@ ]$  H@$ P%$cjb]g;' H@IxߝW@_(B22va⫯ H@ xw[ $P6S%8>a1رc{9A H@>x>w~^# H7Ľe^ t9Ha' .= H@Fx>w[Bwc$8q)& Ѱ曧kʔ)_|Q,^o$  H<ի cmY;N1]H?gY t |$ |1gΜ H@fV*N<+r/4s0&lX6sI@N |,0B[$At#\cqHqX$P/W#$ H`?__~駟 6?c?֭K>{Xti2{bڴiŁ @e%  H`ߊ˗˖-+,XZ6}MC- |^nm٦2dH#K6Eh3q["~? >,\7n}ק+VN:ok& H@M^#ɓ'ƍ+v$~~$G3+F:wG5.ABX;8s1!zncƌItʐ_ݔ$ 6ȧ8|s!cDYǴipH`Pd$.!C9"EG:\/f4 QLX' H@h\.B;py/0񱆘w^nmԆ$yĝlN >A!" 0뎙j9s4[: H@@ Ļ ?'%q#q!Ю@ (.AA<bp#eQ6%  H@!wUZ.M1RIie% ! giO$/k;F|8 惃a0WS6aO_$[!}Ϗ1{ c¼B:km% # gj$gaFsA$ Z\Q:sќ fG@Pw3h:G|p4|tNNZEwwaK+]\M@+ %B8"_l%  H`!#iq ^j\F@A\@"G8#ByX$ fQE8o7~I@C@A=ҞHB +G$  HB]q/ Hq52K@u @ $ P%Tݓ@+ Jږ$  H@h5]K@$  H@{ 6G$  H@C@A" H@$  H@x=# H@$  H@! ngk$  H@$ F@A<͑$  H@$ PH@$  H@# `H@$  H@@{(Z$  H@$  H`Pbs$  H@$  H=RRIENDB`amqp-1.8.0/docs/diagrams/006_amqp_091_message_acknowledgements.png0000644000004100000410000026502213321132064024767 0ustar www-datawww-dataPNG  IHDRuxsx pHYs   IDATx]%5>م,R4QzP,TSJ"ETHH-dNn߼޷_ޛLrI%I%5BC D"@ D C"@ D"@ JV"@ D"@h*mCGF"@ D"@R:@ D"@ m#@mH D"@ T*Y D"@ DmT  D"@ DJ% D"@ Dʶ## D"@ DPd D"@ DR6td$D"@ D*D"@ D"6T*ۆD"@ D"@u"@ D"@FJeБ"@ D"@T"@ D"@@ nDxW婧v 4Hk.YfYh D"@{T=]CTuYGy睘?^oFnA~T} _+¯ٛ=Sn ;LVU8qWj$o饗 6@k/g?9Zk-yİ , K,,䒲ˈ#bxS gYF- (R|MY}K|i k k#`2yd2d2,mH  q_k"}(j+9~$xg y7"gLDt>4vm%&lRZ_^yԔU#JBI9眺|D^]tgmҩZRa<,StW6YUJvZI֒*U1]Æ +z% TlV\tsO{^Ix^7QV6mZ%OJFկ+|D@`׎뮫AEfe]"{GlӍ /A/#{~_SLCD{*GOS}΃=[Gd[Cܘml`}ر/VbnQW.bԌ7Ngr-3~}e0fG(?y{nֹ妛n-f{[.',~' &L` Piqv'SG|ᇢ3g= P[ a^P;_}iP^Vʖ@u/~QnVˆ^{-}>;SV^ye'7X?TNAJ?^zG,7FUpb ?\ S^{-AQ^ad~p謡2~e匢~׾fb ɷ ÊDtIE "@ DPlЌ % #X0cƌ5X#6[qRRzf?Hvf(砃Otoawyni;묳l6zȱ*%2K-yg϶Xz ވ$/ D"@@ Plk1g7\r%lƞDsӊR.} }tYy?`yVZfYyOO5| n&kwqsL;b#fL1P 1SNV>o~2ޘ?fvO?tlvyroNjA~ږ=cvA{x'EHGTfQ`98b`qJ@BTD[x;eoVö$'4P)VS4.h7?`i:ƌgl+ -VT4jO磰[n)JXv\|^rE>yߥ苄@@g)o_c=Gz묳ZҎj 6 zY%Z e,,%2\c8pkFډW\VO+ҫlQ:FHΨ"i'Nfm{eoJHyTioѾey:vѧH%]:oI%%=쩤K+҃5יՒζ3'VSs=Y_v*1ڤ.rwj mXxkݨ\T 9 ;\ҽƵأQrp:TC*NrE= ú/铄U))r~(e]^ҽky{ b9Ŀj~}X]LɎ3ꕮ(Q-9fOE=D;6|oFi{ ?iN:?K: 9TQ./Js=wEA{UZbqbv-wӏ̿UW]5Rmע9vFY連:Nֳn~nM{9#v/Wz_:;zλ<,!e*l,ݪxLT0P$![#{8#-#}ѯR9pt{k6x(DSh{RK6|`!.TȣTKz8S#ݲ(C^CF=mI~:PB,AI:VR Ee?/~anM  65J\֙"P?C}DgqNdʛwrO{t(34SMўu }f_ME_3\@_t E;˛fJAw\k =JxWtˀZʠF1?uʉRA<]R]t ,]g+hc9*U*2QB;S*;i>z^0Y%^u.9_!7*(ˈ4P41S =PaĩR%/2D҆2R/6t9T#inG#]ZZ!S?у7J壓KXzȯܬ̬q> w 4ctYnA<hvG_*P0|Jot()L@^l j3Ki/_숞N\l1.U&]]mJ?LB}kt_U+X386!f tfONSK,@(LR+:꫗.ᨫyER % 1g?[Œ~ޤd/P'1{vW41Wˤm?7)}%cf>?ڴ_veUC<-ՌnmOM3Je/xVMYL_-[ZϺ7E?Vs{փaʙ"i @ea9CygSXJ tt?^Ý*,Tj *xa), :ΟW*KI1[A6I;,uaЪI f ^i0֪C ,Wl c[fz)VlТ|衇\ '?䮻hc tɖ /T6@yw<8ڋ,Hv5{{m)e7E NإKXb|3@gX`V}/ܻᄏDhq~ ٜݿ\zJɴetί.,$;=j Vd`p eai? >≭I|ԚOq8ljRkT]YX.oTvz{VϺ]-i/hWi=3)9_M6T*G9 @RF?bC6FkAyJ%FAQFKkt~1S*NӨ|:eKiEDJf{ⰜU'<U.Z`*:da EڶT*1W`i^1KR̀*&]%vzF$cF àts``jrC̖3+yeP&jXrj,#/L ;,όgJ7mR _tmV YL7:>bldgαҵԤwD 7:PvuWeEAPІpat#y1? ׉yN=8cxʢ񯛘\`_Ⴜx㍹*o:,NM58(q\pKzxÝvQVնLx_|,M < 672Xz^ <Pv_;cM9PZҩRQT1 ؇pI!OyJ~ 1eiu2-LJǧY,vLw2jreس֎I_rP|\&:ćO.Y{z):i1mD`[bJigf4zܰ=a^+7Emt%iJ%z8L6c=yhIO;4pIyUkǍGQ¾A\耺+Kg1[Ym>?9PhlS5c0ιoY\ l< u$__S*r|W*ѳS(4m/׌Ri=>Aߩ'|?T/,ưzԋavr#)ˢtc8 N+*.u]Wt0oڨ?OߗťRU޺7UD7Ttoup!.ٍ4:p実u|Ӳvcdz\‹L`pnt;4QRtkSi5Ry޺/޶Z9_+~w#L@@.hрWKYA!z*c_37:(P8`2WY#x!~%^mOJ/4Y {ǚvtiPg'EgE=_)It#z)niڭt{S޺ /fwݫ9tA h ǟ'Y{6]hIӉ >X5XQOkKDg-}:M٩*!u4ziiR--y?^g.a݋גb 嬑љ$rlCړ+ĩ0CaffGgqEOж5\S0PW*u9xV7StKGϰz#,<|Nj.9(>wT*$ŘboژL^wfбe6D8{8E5L{ivѨh;(HTQqӬR %"&($鷯DoDAt0{]aa53uC:]Rv0ܪDB)I :hz(7h%`3 .+;EA08i[‹.œ.(K=G(%V.i^~7爞+XK%r7b銋ق#}7@g{b0jme],NxW2nhoNˠ%]sQ\ NTvgjtXzD #ِ҆6L=j83T)Y_Q]$.LD\xi$˓1@h=m(2 ,WtSZn lb~'(x'àBĻL(~WiY E׿,7c􌃚ddE[I(| BKd11t+M`\33AGU|kJ~BN:${12DS'DFzmP0"%%,:'=;Ng*'5W-D;Sx(0f)s'Qm(Un0'Vm!64d]3\~1ӂsLr0;nV<E;s%3=,̣a5="<^fq*=ؖP;P0M/N*]IԝJ(0P&S3sd,3TBhWtN~.Kioע˭tVNKFa+SWtZ0uf.=LrV~ C| ^nw6̖@VAYTk9 ̪J$%;H9҅eޡN=>S6ǀ`.7|sΌ[di$] W(yzڥ*fAF7+ޕDP"<]U3 ^7g-3tK=գic&EbO;.,fkx{bφa+Þ{YptC$^DXɒUS*w dx*\Č%WlG?iSmVեty*]ts/AJeŧ^*jӂ}MIeX.AzБKn'Z-v\ruѺ{-Cp_ml(镘]g {7l;[ ^qဤ|>/ d`F'4F ` >bc^?Yp2qU Ձ lrt\?d Aw$hҁ M`)]Ž~nEtEʮ.  w,mVkN~J7x/N%̜b0#ϱj`?_ bez>0YJzꫧû"=Lm+L*Ud"$-E/ڊL]ʮ)Khuaj`oWz?U ο IDAT2fe.=/.q*׬0b/ {1|ɶwsPx'vfmb~_^be]ficfDsnGoz4vuK-TLݵ^1(KOO Dۅ?Ւ\?k2iCZP̀AZwj׍, ,m@Q * :Ӣg:[=8PG10aa_ӌ̼{OIJ%x|Ŗ7IܯUF=ʍk8ZZk׿rk7+9tRxT`,>%wO9z$#t0Z`+Z|0*[ˤ !f*a2˘;q~a4JM!X!T`Fy Uo.ޤ/&䟭+9Y4}x%v1%Um ݯTGJ[ ]Z@Wˤi4[JPwjjIi]btiJI;%wI>)p %U8KFAޒ.ߪ{VO;6ui*P?ӀrNP+i/H aILN'ɮJKۢ<ĩ**&Z)*hw=L0,#m9 ugt8ҪSru[UZ=*Ҁt:XU%%LуRzR%K#ڿ[;9g[:J9i碤-Q^QrtVKuc<:TӔC[e%))+ K=T9nP<˼K Iax6}]L]B.:V҃b^u@ uSgKt殤 T >:#x {-}gQjZiOb u@Y*%]YY./MIF9ȃĀv+xTѮJ+*ދwUigGF=d(Ɖf lQWZJXx6yN Tsބ"O43( ˪]D9PR%YRMFbՙIjV]foۮlj KZj|EᏎN3yY `Ջ?S-G'W>-GuT(-xk J%:̺įy \rݮVnU]TPo>zz+,:Y<;]jC2%׃ezE_5*}RҗfC)L<*:\{e,YI޺>$q%]ٺ\tsl2uAMrPA iF,jzmO5y֊2VRYdI3J.se]fzλ<.3,CLQO㵊I[%_Hh 1$.=X8O;B'%dN,p؉v$,L_R!h:ߨ2 4 {X~0nTⰏ,FKvO勽pKpީ4ɗ9$VtЗ^zD!`҃ρ!v'6C 'c䨇vS0/MiujvW+\FQB_({AZ^%ÅQÅacE? ]`éNmts{k7ύ,Fq~}P0Q&~ݍ.?~:#.:8 =s:'"`UDlIBD7"пRٿʓ!DEGyaYI|2Kk%Y /V/`f5ƈD#ڨG ,kX2fH{aO e5G#V 3#6JAw\s͘[ZS}C8 Tu{ih"%pk-Y"0#/ ~30">qP N34~N}   `{k @FJe.^f"0c#SMj+cY |SOw޹È[pD9T*y3{D" K.>+اnI\n-ѣG*"@ @b{+H ].*,&"@ D"iN+"@ D"EPbR D"@ N+"@ D"EPbR D"@ N+"@ D"EPbR D"@ N+"@ D"EPbR D"@ N+"@ D"EPbR D"@ N+"@ D"EPbR D"@ N+"@ D"EPbR D"@ N+"@ D"EPbR D"@ N+"@ D"EPbR D"@ N+"@ D"EPbR D"@ N+"@ D"EPbR D"@ N+"@ D"EPbR D"@ N+"@ D"EPbR D"@ N+"@ D"EPbR D"@ N+"@ D"EPbR D"@ N+"@ D"EPbR D"@ N+"@ D"EPbR D"@ N+"@ D"EPbR D"@ N+"@ D"EPbR D"@ N+"@ D"EPbR D"@ N+"@ D"E 2D"@ D_ P*z䣚_"z&0`@j~=R&pd#D"@ "0mڴHTLݑ"&ȁ"UymF\@jǔAf9?l"@w}^(/U,~3 J$"w7l\or?D7jƑ@F mvFFp?3& DaԩS壏> &ȑ#+NGGlGEQfe:t7;ݾgJex\a7*~))O7/ hpJv D"~0lL"~QG%'Nqw(@`РA2b=zp p !b %+TpZ2V* $loDnAx y[J$@E w9O_}|e1z0p"@ @'F_$kVn&2#Tԃ{Nwet!׼gJ%;C~ի?[ʪ*kvlDgz_ufJDʤ+D#C=T?ɖ[n)~,2<Иc:@?Bm⋶fر6z gfe ȶ7Vz?Y!D"бཎ }aC.LAd}t^toqL ieig ѣ生t\pXz9aj?H ʤ+h8P(?Q|;j'x5\"@#Y|,[䥗^ PFkӑRK D|o>N~'2i$03ɘ1cJcь#O}mV2qwJO~և:祗_UW_C.k9l0>7[U,z:bR(шxEVZI.B*K |Ͳ;˦nj3|qfhy"@ x_-P,1SO̯ %G.c; ?%ȫӐadFgcʵbV*=44hOSc=sq2Dr-gKB v{ ѓ"@hG@0xlJ+~eimZ;3ɕIx?``e?T CluQ%^_Ӊ VL[{*xľ eNk%%D |^b%gEƲW,EHU;["@ D`:#pL%KtͤNUl%:>9)ݓyh(+tf):d@2VL[JGٺR+q6h??^&$MH "#f!#TH\lIYb%%,iuY;#S<hC8ꪫ˔I(ha` Dt KGJeENTSBi M~כ#6M]mxAc{wșgnzW(;SnV鎛yGf_d,2WGJ?\Y24\#ԞyL4E>|2yWd_k>1|RM1y.첋փ{d.(hn{"@ D8 ;'[ %(-qҒDTL fc Lê%B۫nǴTGa\L%>z4=\|r.Y):,y˰dUl f̃egǼKc>^?<}3r/oZozvr1ЖЦж$^PP()"@@! ww}E_$r;~!!I~ &~Xt8+qke#񆒎`'[;NBw1'#L%6Y\{w3LT" ?&Ըt~?TdW\Ȩ?|CyO>G#l{˜sΙJMMw}7y3hp"@ >>iԡ_N~hX?h3ȑUʡTVVõTz$wr@EBZI?m!o}r)'撵NZKƬ<T~ݰB`ɴLIOZx2+f@6%2gF*ۯ,+l3n>wlrB -di ڗ1_*J."@ }+xOWc3nLK(P"(Oگ?㼾U-j !TBE]t,"rŃWȦl~* ːCd@U+uFנUxosCX8 grcqx>g['K~xNge嗕>P&N|ͻ& c?#D"5m(= OSh?_qE2o~80įcKy=MIMkL%"%:4}/;]d/-`bf%u6aLh#C|2`C׀:>z6{XYaoM~ OLT+>#= D"{mYyʣ=S@*!s^rf*'[?=T2ߖF0ef$5$3@zhtqF2 f8LfEnѳʆgm$ 9|C{セK 3=W*y="@  Sf"-V!NSP 2XMuM֕J J"H"ol2n;Gj)z- !I3z|Fʌ>.>i12ǔ5uSu-B2G.K,\uUkx@J$X^=U6 I Dt ևlm7P 6s{0!~zNTL+z'b2zYȬ_)]]eYTYKf&mO#TGwJtz* 3 'G& ßpǟovl&T}l]{Xߖ#:ev- ) D"Їx6xgGMcz.Ҧt& t)/'~E+ֱpӊ: g* u.B{e~ǖB9adRG(e  ٦HWjlS= $SJ2f|T<=ˬ0{b!wun\ɹT~z_uHSj[1EMU3@AT) IDATJ b0PaoFAa57AuHTmfdeꀩ3҆eSN'De6_KykY% g"@ t6:Qgrc,x/gdqf* J -7FN婲{lUCAvE ! ̠)e1SE]l9|ġP{ڃ]RyӅ4erd"q|.Gv;n"@ D"`Jf v<xnNp_R_ڬ?^ {TR< J )뮓O6b cQt ]qs _P }O+*Ia/+7@OJcY?Cp.0]vP&7tP- Ydۙ"@  bh_ri3#-$yyNoz ӒY)䧭ݯl6śѦahPa\9Lfvhn. ߣaX935{ŕكiAY _uB}pQS,X"2Y(3 f{,/XYkd 60>ȷ/o`?nP-_J Y+P@aß+vJlf3xj2~ "J,*{<*vEOӣ[S} $3K.d/,!w=gevhUYjOe_%jF{=ȯ/l rVU @ Mi%))e6;v7j*8dǕS~&'7ՔV*LjL 8LqA`4GҠ<|kHEX*@bk[E>!D"@@@Ԇ;G/;L]v41'N +SO&if$>_w,22"˓O?O_xd8SYAe]d֐堐B(\A 6]C݃"s..(lfqaj2t Zf%d= gh`+L?Ãb*DB, |BVKXWLDZ4D qP5ڐ4Y}{BCv͏>SWg5[FVi D"OFi!,Z7/sGvN;u֕GR^CYeէT~YBJO9uQ-m~ \Ke."0J3fʢnzJjx`lP:1WV$A4TUE]˕Fٸ&J~AABB mԖZTu4Kb}I/U3j& "@ Dt5 {P&q%|AiߢlH!M_~yyg;,zT%zQl6>MqJGSL )S$6S2l)i3/T9:3AqLpW2; YHee#fMJ ʢ)voy8<]!l,R:<&?D"@ZЩwqf*lʣ x?%OסCcO292kʜ3a>)D |)pg{mP3PS!4zrW"]țTYI !=^lAP=xL(tfua+2d D"@@g!%1S-ah}S{ZFX w+ƒ 7j/=x[?!>**͠T4]p܉J{\4efAğbA>JW_P ?)e0q1S1maU,B%bMIߔLcr A|B!_~ }y/˭"/2h D"н.wCԿI#Ӧ)& io#Lyw/HC~ObA6ʂlF}'J6SiM)Э @3~¨ \P;B g]8\Fgj[2~v2\Mhi6i aҩ;"Da?Ly fIђrɥ!/%3`-` DgaY/#@\{ؑNʏԐ'ȄpS!#]U\W<=aA.HxlFgzh*"L3[c>* 4N!&(pZ)77 pYT1eh` ,5.ӓ&%<,gaOm^J:E|9Ux Cf,ɩS$rϴXtOn}p(:3R^b!S"KA:O SIg#*uW7'nõC8i /ޖC*kA&"pW6&(Mx uB?(?mVVGR0;iߕԽ)z悂YzpH=25Sn(HH{jiD")ӦX*g,&t*GLiBFl \˜nxhsc?zW~tv~' #[E{em,c׎*f&?8iFOMq5fRm 0#M)21T*[oUFn̾TC۩5\{A KMU- E*]ycJ+vt6+3>y~H+Xm=CJxɴM|S cHrhbLTBX&hx6[J.R9~oF+P(ʤۍ0otΈ}Y6d7 ʣSiIlz?OӋm*D-,\QAG( ݞA?6F4ǟ` Upf,˟iPՀ4^^!f6@E݃X V6K eU1%..50( VɴҠL$B=fUI\p(soRai{ fBU3YE<tpCeӄI/yG X!.bkT&ߘ痛NYf?8*xY5TޘN|Ouz]-ʿ/Z~:k諲%9Et~h}j>춥--tt?{IHH+P5XN=2df,?XP~-fk*-CÍ$3tZg%_S9Pʼʔ0CVauW<ʅ7${+e2(SnM6nΠl:Q4V2U\~\ؕ2i;2i.տ LmwG!dj9Gi\ r ݁?!xC&>!Aѹ .sk\Ȯj|tF.C a$C !At.-4[O0֘ltV51?!4,[ g)_ң0j|grr,eI<_9^Mz9 K\:(i҃tXtv䃫2% ewm).*΢6?EwK~%ifYEj67E H_a3jT2$S݁i'yBƮ0ĢjY]Vf)Uz{u+-N(P"mR. e߃1iѸ|{MU?(aF2(HXMԴMi1?d저RYp אCd'N0-!hBZ1qty.4Mǘa '0+ ̂,~rUiʟ@aEDȍM>oK iyòh0 Ro&eteoh)<9CU9D~S#qtOx.*m`J$>:o!J!}DG?Pd&>1KwL RR ܑ"wSM~5n2~`{qhcF5ђjBxXAyVԃzpg5S@#\&O'|azk2&A1JJ>˻K%>JyxO!At{KM;_@~Pd=\.N`ct=4!̫f qyxpCjdxS7"so*»V ~g& ~pY!>׈OC{irb;T-T/u#<<*iLPbi{VO-~XKeMFTQڕx6}YLuT|AeCtҡl ^bg .AaR_A-+Pf%G,R %TN3ADpgo;1M+ȋ,e~,D07#~RZϋو}$n[m>ydIݛ:hO?,1<K6AFW=4l@Z v)cҳ*~0f/بw*a0,N4$ !M\[m6:x_@!ᡭ4[Rɰ2!20a!~˥9}@/ 4!?T2 "#҉H6F`V#m e$I`¯:TV xA GB-(WS,ـR,Ybza0 YX`jiQLgiAB3gA*BZ@ Yzee7GF4+ҙ0+d>+l y|IٍhVa`g~~fe`ИGƟƕ0&g8v{YAcd:q|qpf  +x<>c4/gq䣘Wȯbt?t=aqb%OQvo˓d H4må}C&OPԉ>&ˏ/^x\&06&dTmGPVNk> S:C/O&Z/tr TzxX#8^F|@FQQy 4F1;]=G 4T* ^xAf[xp(VW􂏫`)a.i|2z uϤB^8 05`N'E6KXdkΒb= ң =IJ?z)ϳ2]+xY, |pk G 4 謐SȢ,*E!YGs?',42kZAPi5+:):; Ww[WzPWĩɫ/&s;1Tt~EimϤA;ZiBDFmYC L=-Nk%χFo/) 6h4mtgqCmHq 7/'n[ieo"H( mbS;igZZ{ï\ϽMq'~{D_:/k/k+5!,2O ?_ n$R*GmAH0.FXTrj8/lOJ,Ln: -J \Q.n?Ԋ7Vκ}:˄w$#O`Z=ϟRJe(6O˨Ge/O*L*Fq(!(Ѥ=NoK3weg=^V\uE JdxPm ex -k-t`$Kj!-/ YC#`x]f o~=NLp!~V_Vf;c=?U,gYg S,'syXihC3pTïx pS,F"^z%YmՕ,T" +oQ0oZ, OǛ/s|fe֑ڲ׫/ZZwk%^dAPBD~OәG,'CLV D-{/_>OR7UP!cRZ{P Ns"t~КDlWil"r{ąW4c2ԟ"V z[o).uXU IDATOE=_٥8 4* ء˟6"?8?ZYֿNEEI볩2L+!륵{O~/N?ޢKa>EFV 5y$m,{gI8QD58^vyF[>* \o Pg|V~i9䴃eYgUQAU޶~  U$+gd6}gd!K,î2 FwIgʥh ϑ+{%k҄r·W?婇d#/Hb=ykj3 l v K(VWܕ~Nҹ+fx>%~D" j[Zudg~6C\ſ?x>C{CI&!Uƺ?XOObSd^4Z+|zٯV/2m<< ?BL2Yk^>=Ͽ_5GZ*IzT=EʲI.Gٿ&bWvGS>pm?C/{]iO|&r~) 7lr띷ʰdv6_}Mxrguqe=߇vPo y2u 2CyX2hҌ7NrK3flfߐܟlRTeG}~fQ贃foU{<O#)OkEB B\B2PP$w$A)E2rYFEuQy[j1{~B.$rE]DOk<YhexVf%k{/NnzFgeJow׿UttY|j*L [A4nmf̲^I?4Ԯ0h#M1=Nh#Md1=Nh#Md1=Nh#Md1=Nh#Md1=Nh#Mdg٠8T YǰD{l&&}:\|e|U/tT$#aj] >LwS G6L2^e-;n[76Wb2ɑ}JF xV4!(tc:6{MkdgFd|^s=j7!q>!9/y@dpHn>jX.Q3xwj#׈+:OÖ+Ȑ&|vda(~&x:5m3'~$Rvom*&J"_P'B&q~ȱ[I)WCol2j(ye2qD~emn9! C[3ceW`fz{\؜,F"&M$FSPS쥮cuI=`AoTcYkMp)?7K݋|Q|Igy5,ԗt 23M+fk|WVMkS>e!"WE+no9N2je/ǟ҃T^[Dp 7790>t|u_h*u{|!e?2nܤ g+M~%fr۲ΰyݖGXyzs22לXН,? K?p_ex.Wd tK`Ηɘ/-_|rBz{^N8vYd ǟzN9+%2mӥ$})_/ҏ|TWh2ڌ??)W U УWCڵڊ s\teǞv-em.:L琦'tF+cebiA8M[D{Aۍⅼ~7,)2ۢ?l[Zڎ8f72(!f3A_kZ&_v!o2d5"knpLgwGVdWKi]}>=d#^So ,?V+ݝ᠑eqW6`}Y~eZЅ?XX-f3U,9#w!gq)1b~Zz;2|G@h^s5 + /,gUSO^O>dJzȔWܟr)rI'o..xr/._n=\Yr%BlFrWˊ+(O>{׷-/!?V1ƥV_PgPdf aK+-G]= 9S3e*}ۣ*U?Lu혚>~(]o O}4S k坷ߑk.FٷHؒfMzޔ^3FQ(x7^YF"[qWoylfFa,!!!LA7St V\xf/@L" @ng7䟡/HUeA^|]k'גnӪڑD9dLC[HL|+z= [|,/<쨊QT9\Z_av#7 UAZTƎAf6D.9{Z'wVX{HW"ر+Z׵]wWqW]+mUEŊQA w37{_7䴜d29 ɠ!{1ϝ&9N}*@6iceNg@6i1~ :|@; pX֕_ve;WV`9c7&3>Nh~8s< ݹlܶoQ~ sՕI&}ݧt>:_5v-/22|pՉe]&C;*~'4b%Fdiڇ}l0!ELrr3LrYN:O6GR-('4riu,Qk+q₦fd=wۮt|mFŞҬE39]v[c<8&o@?ew3Cc˵W]+LXڵo'oVɜ9sqJ]K˶-e3srӷH-)oek1g P-*TrB 􋃸 ay+#ɐ1EZZó/ .9kl[^\^WrQ}dJeVnL}idW#<~Gjا$tRNژ]unR[ߵ;*ꘝwβM/W8?L}BءszcB:o3$: ޚ*Sń_1IU>c}Sѭe0I~t38IWXٱ7P.:ctB;ro+wGl"= dAx"ܫԃKggV-v֖\5kUunr[!lw/,qwQzmj2_oޏep&@&r;yn vy. Ⱦ;~| 8- NOY1/3R_g s$V;wh*w%{J.%V^+u%w54o|->p^۪]⸰^@2l-ۦU#-7mRO_s5AQ8]Kmg?H-q=|6(pGlj"hhG}.% CnX =h.QWcrp2i; 'x"\}Z˃#?ҶjڨnupötW^iUu x =ߡ3R:μwPm1=`rMgx]6+ع}악z] p^ O?r|*3ou|vS~UW9ia<Ġ~ ɭxvU)x Z,KljҐߵc3b\U4+Ymͫ]gfi07j%<迅荷ƆO/cFde?Yrϱ\ OŊ½ޫb3?^\|S8ee޼y?J~dܸqqٳg%J99#e˖rK>}c{ IӽEGvȑrGk/'|rr,Ա%l Z06+z»kxho"ܿT/aEgL>JO81QmD?TUZy~[y3@¡Cq™ukךWO:K*.с' N}K8%ԚzԯW )bˬɭ:8WS>{C9^;ށ}z,W48+a?m m7sNshn;\U9z4ihoÖW :5ܻw'͑6ѐ t\v_·MLm]Evͯu5nͽ-_+7(Xa)MI)`}.Xق\\d!$@}{wb{.Ǣ]zpV\aA̷E*Mzǃ; CKʻϐ{H]7>[ 1yl@p8qم* v_qPu jv5?ft/1֜p\=P )Godr`?J8h<(r&v\m]qqִѫ-Y`I0q79eORF=m`lgi1?8@U]vC~+Mjhйsg˗KZ|˅+WJ#Wf̘!o~i,ZX ~Evџ]vEZh+BV}$X51baƥ+xu5u|=y馩:ݛ_z56ȧy0 !*ޫ(:y0bq,Ut&vq>N{c2vZUAWȷʎ:ϿYZ;~?d|u5:6 fp!A6鱉TÖ+;'3>> jѺ:;m:y4mL5o`5 ٵXGԅ(j8̉&`KaMdt`b2H"M:k9s qXf Id))Gd|PY$"$|t?;yQB!ϋ:[[8>{td>Y&'Op8{B}p+-dMƖj\ x1BǕ(nܢGk di:[(: xreW'Y .W4JAos;u-ff3b]Q"?\?qEi߯S ~`%kV_Md5ur#VS^}$]լ?!;/w-}?|㜳JUb kYw;d ۱.<+?a $;F1 >:`,A/>[,p.J,&Wi_s<nRFa%leR͛K:U4plt^ޞUB IDAT/'%dZ| F;,)\mdX,4 ߇ԶٺOzGvyN_\gkLṛ+NnT/X\6$:pGx>,U/&I28ˇ)\Yd-9p:K#Z*fJ&|~~[7¡}'<{?q`|jxz?z4p6uKc0Xݿ= na eu,z+ NǏo;^RBm^2^lw䶷+O9[l@mvq%qI1rK?cɓevHp9&g^}%̞s9M }Hp-^LR~\B27@q1MMi,o׈ˆ2^o+7|zꐧP8IYO`~wkAiJEIG+$E2GӑȽ:yuՕ^{)۫[=SS 4p9s>nܼآTCI\~uWF[ E"$N jT.uMJҸcwH9t$IA̫pƄԯaP&Ti`P\tv4G0;Bvpd ~0`~I&0&1ԋ85z)-iHsJ8_M )?t{ -me]7,438oabΕ5T]Lon;.gǟϔmh?y [ eͱӄvGD =R*4>@S>qm9^vW՟M2>2~~ݤaZ.:}Z@W0ɏ+\|\m ŵ%h֋:#څ8]rCE>$|2Svvpɨ>)]3؊ [-Ž(n%`^_z1 .J׿$A[cO``㜃6-ʽlDЋ;.x\iiiqJv$,EojM+VLԩx![}W`/"9Ӕzq5jEr~'uj5c>/"o|-c?^$l\廔ԇTw0졐:ց{F8YLk̕,LW* $^{osΤTz'9,HH!-U>޹]$,pZ\ ӀNw41T+iCN乺8t"@kCa g]o dw-iGo45vKϟnKtL=XQ'ıTO/#'j?$WHgb_`M@ ̇_\i^퉏Ԕ!l.k6191:?焝diɟ/8(k4Pw6G\v+ms-iOzw8[ytfv8!gVv(d Ig3=C.Z͹v 8ɁҐ瞀fm[f؟gEO?^;Jؽ'me;+.R|PAgeqs=+|?s;w/b;Sc#\Kԫlm2䑥5`9Cn{+Vv۹{otxaan-'c_Vmټ⋫6{խ!5OWq [og~7FSS:{vԅ+t%[jǜ|n;" l.ï?$I}8,|?=+>6^8lo'8JFdұqή.N\5m#LvمNF@okF vU/b9:mg?-6m6~}69X{r찒α?=ícŕ; _\\][ڷsLyD8W'Obs{&ziW{ݥcU:US/zFϾ-s;Ѽn#>X NI6Md'fEta]C٢}PZ4_Y;8or=Ef%ec=>6djZn-w%=EcG߿O68?3x`ԩ:2]wݕ 0Hڜ}i~LJf(z=o*p\])s4݂ )ڈlR-l=)תx:~*z8!cZSU5`a%;T5lwI<@ݟO>?=O  3iN%2oZ&]V1%}x.c U''lcI,ꜿs5cjD]ia4ko4a:j?k)2:z0mbb#ls;@? 7Dɯp]_[O9Q$25˟c \Gq;nN嘈Nj!@Dn2~D|<=wE>y\E,¹JtE/ eR~$-ALz>ɇjtvB^R]:]Ϲ?-ԯk +J̏5մ~fÏš»v s#w|3buLKlqًะ,Q,:g?EhIGerew.I:ibe´q2Q?_:4\& [W~k1 طNX5~spv&q]< $T5%ʯ32ns ߥL OnuCOjkó2 n0qr͇&L[yord]D4ʍb"/~/~mb[PũvS Ȋ:s%q֬Yҭ[7?+P?ӰaCi(g@| ܹs1YzM6MI5pA`$On77+J.V,=󩧞f͚IƍAzEӖ%>l fDq-i&9;n}R(N!5h:5XƿuL}TgHiat&Qe P 9cs.I~\,Wt8$@h90ꌬ*Pt}&9`i//TF8"ۏm'i} HhLWWߤa]iҋ< IKm,o2KR8^>'[l޷LNiS jVU6,Lt"2᎞Ź__eˍF#3*wGWq[D?Wc#AdC,%+>gҧJ@HTKhLo L;|jEA>PzWU5,q !>Ud⊱]IRn{rѭtbr"=/"'ráX셭>ICMI fx_sɓN&iz| 0]]3yPnfEpBv)}@cE~ }WmtB=}`<}ζ- Fh&a ]@j04"%:EO?'J^yd+x6 OEY'=a8AL|+7z{68:%+ȧ#@> ~}JC eձV_+љ;8Ɇ ߩ2l8E8,3;X$]\ӯ-Y[^|E)o$_#BΚ8idRGɟ9 {:NrB֪Yfp "`w$8t< d%9t,I;L 0&/s_aW8,RtKh,XKW&< |L\AB , wOދLZ`C r S| tąs=D@ P/A窄=MU:%tk=Xu&jiDrw8zʍƄ'J[  :ERTy|OgWB9Gx/Sy ':y]NW `N1`xApJpH5Su&h%1m弜HPBeɗ,f#'nO $/,',L(b+?ek}r]Ksy,_U`Y` -v@=BӘY&tL #rكEOy^+1XAdeԿ4' I[!TJC,8yY i49oK6;J-zGьP]5 G/v;h{(NDFg17>eSYvەr82K+Biy⡌n$~M#UcqN*WG4G:oJa08e3$h9|\ҙ"Pc;w2W_J+XӁ8m}ȓ3onKJPƖ&e*WiDpEj9k#E.ρ G)\ϗ &}p{jn+4{\rrL8&mTEnCJ[yU.T`% n`s|AGRP99nt&% W&lo'6d7b::U+OdiG}I'ЩW]xO!@r"yEeY8aVblQ<Ty9R~/—v .HG͜0A:*+Єa ߒicat`ڮJ _v|]\ 1n_SVIk:+,j=yz'X0"=^!]#Szy䳀eO %pe! 䋃h[5&ze81f1~G?Tda?eX(dјF~R{sIw_P h_4B!YàXmL^-EOy ѩ̣1b5yHY:GS\וrb{#cy2Κ+Dy-=yC=jX~ 3g<`g] Ndu2!4tp]22#Խ$4'֨sJIȓN g))2B4N>FZ! zm|/QGuu &?支M2ԡ,~Ok@#\nP6q qxMB+qԈk-ʔxOrr]MNXI )M7ESF c ʍWIЈ}LXUȀ%I&L''TIhLnjODUC,4ːc |ࡄL(zy!Tۙ< *ewP 7ǔk&D,a?59N" BϘc!2`Wlt*Ki8P~!Z·T坳 N}@t]O;|%C%dC=,LG 8QM!/)-Q  #>Ӂpum*d AK ÇK lxc5ŪUfJ[z\2?`p=k2X+|J]jUf$,+oK͍wZ=&CwāBڤ &rZ W$Nfjg 8g>dt*aW?pڑ&;>&+uHq+I'M;$ǟ}#t̴_Wq<*~ÕW:FR)Kܟ $e˖˗)~h#sŒ+E9HWϵ%TLDNIp2VT8]+(rlU/ӯqq|%89ayyee$/>%6p̩WS8'"lʕ)RgRІ*Ė g(zNt5yXA]4+C s8H nDTMiE_EӓCt*hXե]*Ç v7DfطtLzYjnix2ODp0wԶXGi?-ѝͩoN#ER;yJ^t&?N'pH1rsJ|JcnӠL1A$;wU۠t@0:幙 .eWЯE^.\a]Ѭu(vkkKVEʩeYVDYʋҥ*/MEcdJ wZ'`ѰҌb̟Iq8PvC "@>yPl؏pp wʼdN:$tI! &e3# <ģSP{?ĕ+N2c[{ד9[!u1!t8SK;WDGjmzAkts$Y}C=gAgm)i;(/}I\_dY*%/o=K_sKٹt(ڨk>d ^Q7߃t*yp M,{Ra1&L XPv[ju'+2O1mW21Eעԥѩ,G;MJ/g[t+e 7Pvi'f7ʢUġ"[9xm>֩rEI,X[$mUJkqrK}O'O![$Y~N!rYz,|m}G90nQx.fL*z<:4XO9םG~&[<3 +pXn'kPLYm Ocd kX+mKJ{-E'Խ;FҒQZD(ӏ3~OX&OLq1T JJT,۸ g>ݲ[u};>#>w:|٤K $+STKS8 ' #jgK͏C9Е%D|X=ኂS|%\0<X>eG+/;y >tt{۠nWuCR H8ߛ:SV c W99gm3QPG{z yü:(cO*:Q: Ɵސ/XZnmc---- D]9 ̴q^I9w (g (zRqWtȮNF)UP>ux fjk ٿI_|MJR+;4$^C9č-hpneΙoOVUnj=}BWS〩3w%D3,w\sSN%MbN'W,=>Ru8A"?Ie 咋.I)S:ՓbI:otBF`i}gdBKN]>xP,_ey//.ZjHW p Roq=x6le]V2>okj/WM.2cu#Q)&^˯Rqɹn'6Zw($J)?S&U щ'(̖;Ciج~ݕ3 ,g%c*GWFG||'OTǘ sa{KI2R8Q0@p* >{~@q)Z Z Z Z Z Z Z`\è0¥ks4W_$M{.ڷG+M6M~(or-F="-5NW%l綊Ne Z^v]z\RuҴmSu #ulPq`> 3֠}R+*a_7+ +ynoe ma5\[Yeh‘L V6YjCuAҒJoE D D D D D s2Lt\,ML]"g3rB#Q˝=,Ymuc=}]YqxVCt*h|%6y'?xtغ=5BVS1tV߫ yKT)iI;t9~Dz֜Lt6W@Z '>/{{G1D D D D D D D l%'^lBc[?GRʗNeW=^ZnRzl}6KM_n%=:}(R <g\jbm/'+6iJ900o$Bim؜ɯO{I;<ꊫHC@@@@@@a:6acYLjB[RG༌pD)E%=%J2˓Neyh{o|rac, ׇd(<. G'+tѹRs:g:mHaL[Wi+׌/LQόwޙbp,lE$=ķYU9dhف&+_ SGcVmʸʕW_)Cvtg-j*c @$G&\&dQikijH[n[suh١L|4iDYS@@@@@@eNIDl$FBA(3YY`"WacNeYU&]s wvie6]E]QԇXPMd\3*gEz2"I^9|f ~\ |Sf7KnV9ꨣy\@@@@@@dqMy)<#=l헗.:y6hE~edĈr H~d36=[ JHړjwr6&Apxz""ɕK{BIM>Cΐ\֭(Z Z Z Z Z Z Z`@< +ί s4sHW>rq#x!"w Cn>fiѳlwn}{L۶jO*͹tQe=Ԋd~deը/c?zH:v옅YE D D D D D D l*t4m+fH  τLt*kϵʭArECu\'K,MDzKum}BS?I^H~O |)S"?OY:,yiy,!Z Z Z Z Z Z Z Z A/@s0M3Q2b:HW(TϖS͚5夓N>}H4hU_V^m/JhJ|Ŋ2qɼ˾+î&Ԫp,9Z Z Z Z Z Z Z Z OT-&"ӌC\fOޭ\n~)!:y4fe`v [~3F?<'&]JNIҬK3Y;xҕb YU_o~3ʂoȌfH]dЀA^KԩSuߗpw\W- !{{t}+]1)Ne YTZUUW^%+W &ɓeדe/eߗ Eeɢ%l2Yԭ_W֫+ԗ7X辅l&n6lذU7-------@HC$k T'5G3^׆OaV~I_JZ-oT͔Q5_C@@@@@@@89[\ 3cy+H0/_دJvNeLE D D D D D D D D e:t cHTK2֟ I^- w|Dѩ̇#hhhhhhhhhX@D"Haځ\ΨǏ8~ ./oWst*e'Z Z Z Z Z Z Z Z Z( $G<gi8!$>{ a3+f-P ,@'!t-meUI\eqղ4`T!Z Z Z Z`@2+IjULO FxeGN~ϘG|X1$01\귋er@FRzĩKR*~f8C}Mh::LCS CT3F&CIGrz0cժUwʡ/Δɿ,j,Ύu_A@@@@zḽt 9'`" n4Z&x>S_mW*d&Z`]Z CIFs*Wyhbٴim8Zjx-%hhhhxЫ?b+Vat581F`,~E }Ǒ\ʒNjk?lF,>Bx6P.MC2 ayifעx1MZƼC&˲7\|Clt!>< 9\io%ƍeKd5ISF u,i>Qhhhhhߝ 3/ՕR%07b&\2k!SvԆd?sJTN ,@gagɬY4l2Ypԯ__Zj%ݺu6H5k X9ؙʕ+icOCSl$uqDSYz$&ߏv [-hh`EciޙSyXA Cwvs J{TN%' v|f%s3'n6~3\2au әW`,Cؔ)S/c ?2H1ЙA֭[[l!olVҩS'[Fg,6Ba^  Mf !pBBIqMV6Փ%Bt(y0OPN[.{ul }U }`mB7ݣe@u/HU^ x?6ײvv?!1P\*p:~o)L߯~y?P2+Si7tXf,/-,?dҚΙ<ϛ7Oj䣏>t[h?(3gΔ/B&L [:[+mtA7n,5R제Lٙy$ ᖶ|C0]>7)ևys(WXmV>H&IVR:% "$0f3,3J3!7t17=kҹsg9Se˖Vmڴ}O)y<%R1g+"kbɴHQ=ӹ4Se&N#spDݖV)GCJTrt2g[yVnqQe!NM( (Vfј̢L!KuuN2dfC裏փ]3F^u٤7XW1zkҥkժjzg2˲`p1 ge3[͡d{xr0J%RXJdL^ *K+] vTN6l|v|}TCɘAoFyy7ސ^xAЁJk׮:@^ +J?gAfeˇ#CI;͊N-p( nͤ.V(N0K6 ͊ Or_{C<ʝ/ש_ 8}\J=홗;dsRKSIVP|Ly52۾ 0~&S^F=C4S׬)᭩/WՑI#!Ȉ*_yr_-tZ]VMWi'i% tY:#fO1Tr=pIMae⌯I?4CE{ O;w"3:dw"^- 5؎%YFS&<|7zbgJ87&YsA$HZEvp$?2AˆSJR_y)x;eZ}4܉  @|P^/_4OHHU66 pB0 7, pB0 7,sR9y[vDPۄ=3a!Yk Y%1NݠAt]w%va[ c-o۳gOkeƌ#K/$'Nc9F5BkX-]r.[TEZeP2C-5lԡ<[ G6*nҽדk vz Ho{gr툧GBӫTᩆ5P<_WJͼҡTS{ӇCp),͟6yIovaYD$x*S2pFԕZdQ˷D,)(9j!ƿڏz^w GU'U(s9qtYih8>_+.bW?a{`sA82T',z7֏J 1?qs*g0<7eH6}Vj9r4o\ԩ_roS6iDv]ڷotgG22L>;u$]v̝;WK/T8{gJ~ :d㟩 C1[lx 2t$9lo '-zknFVjKDT@gIwsq:,ңߥHs3 *}Aql7q){O@V2v%ʔuDlPA \1A]I'^?n=3kmXy>vVÍ<$گlG-Ss*M /3o8O^h:,%y 'sǟYhL:UJdΜ9Zo:<\k:t.Fk1ޭZUJ{ /s=W8Y+bx(S)1HfqWLʐRI+;m<0@޴4Cި@$}ywys8 u)]WJL‰SM]eZ݃-VvWJ5艸f ]IӶO~B::e7pJ6q&ſ̓zM7R85n  Iv2OXV,j5jfPkQl/[<^Ljԭ_?Q7m%Ċ֗uTcFVn߬9̜L&ȓDh;|T=C:dd! F,fA GY*y =zH^u]Ց6]vEt>/_.K,QG__~E?íϫ*;|GrW0e7q "n­tb֮];OX!҇vʚ/^VYk.\4w*y軪rh~ wǽܞi[($FXu+=Z B\uQ ^%B2f'wTEeVJ/rz*j WCs0Ա'? 2ћNUB:d z\Zt"o?9+ 茞~%#8yTa2d3I苲WIdǡNl"$O%es'*>UN~}Μ.cW,j'|^;{?ÓI]iyS/;M#O5cɏ?iW6ĉ(E+/yernk͛p{יHם@^9qՐK#'-m<ưo d}H ˿`d J ,Ɯ-?^Z*iU\(L3 YzemHQD_nS'_%eLU2 N+CSt*呌|W446pe:&Eo2u10i-]T0>EetG!Ç]we$_d1I7CFch.X@ɟ"wyzƃ:Hf:/v-ܢ+gunZp-m!nI\t3mg:Ϭ&΂#+5W^Wt?,U_v EV.eT)],wLϪЉ Z'$Zz}ue)=}qӫ9W/*9RMguJ3(*ŤbOz;~u6n;ձ;}w+s[wI ?19+k+|e=EMMS]_`XdL|Iǯ3a~k6BK 1Oe+#_ݷtJ1ȴ*\Sש$[ >Vă ~n:|3߻+!t|>G^Þ~MwGUjKQzV1 ΢Z5T_Ĭ;-Έ0i3ah?gGst*S7tJBMM?dg%^Ydtw/vI:t萦^_~)K.խ|e˖t)-ZvAcǪcw&|A9ꨣU\+W&<@m&Cs9GKyBi쐋GyڶxSm5+Mm\pT-넃Jz}E[ۏFGhǛe޴[/?̐wA(5 O *U膭: o͏ttiOo`?zwdןIFpNwE?ע+#Ae!Jv6RB}=zƝ貯T(Y:=|iزw* _6qߊ7Sup1 }d.~|sz]wpY<݊Ac"]v{`aSe̽7.؇nK޹oRQ3s~!=`/eUkٶ@͝j~ڏ?A<֎rĔLhǖ]{).*Ϥ {Uj֩'Z^{ ^V1k-e.86msIg)]JҼCY>ҹŐ4ID !ztzxV۠Q,ͫBqX#=;S #GG9pJH/$ধX$7WnF}UWs(YF+\s52~x;{ly7eذa-{ѣGaaa`}5k&lr)e[oU_q~(&SOM? ? t*^Aq* oڳV*We=9W+P:&o1<~>qڗJ&y&ܿeֆz2Y8w<)Wc)3c=3Jf-ސ:KrңmzJy^PfOS1U!SZ*N 9vIȫHϚO|qtkWk8:U7es-]ris䰿=1*>ȭt^{v2 hl?:+WluPyd_Aj(=S19_},s'}4l(*eᏳL$|)Ncd~ñfaЙ'H:N0-?w?VޞҲPaղ>_tBϕ:Kdkg-荹&8d <ZL hkO?{0WQE JbhwHbR"'UDAzkBH$t${}ݷ- 3ɾsg33ϝݽϞ)av#UM12@ J}\!t2ƍe}/De=w~i!c[/OoF:p¦K}f$m2[v LUwCٮٮYYũ䶖uɧt/?=ǡy!S|o0ҦkD.{YߛFaIM?{߰4a#v?{/=n1=ty(]]s8v}ޛ7x`rY7 gNxKWNJ sYaa|ئ7_6:= + yAXLJhS}>:< wE /0&oo2/L?3EA_$S#?\nHϏQ&d}!ũt_~5)cvX6n$ jT~뮻ψ>㩧 K9vX2mbA,z9qD>͕uAX˧ʃ9l 701"'t׏qUW {z~[x%ЊaiiyޅO5gC%Lfb_?oUodcpGs-:$̵ a-c3j+_PgM|L3d%_WF{i.0Hݩ{Ű(<# ysyޱuj3<);;:Lgޢ^zZx< }ּ1xĕFFv O}6,U`쿆AklfܙO?낋[MiMwqCFgeVf.=rg#EYgM$m>3Zm:6wޣaG;~1ۂK~GyϟsTXlhm[ 7vq3tjNբߟwt1~`ic|F 7r1EadK<x6{v^ytts)[ӷ k9aӿ>fY`D*:ȰC7/^v~ޖoo0Y s-6,QF{ +t@8fKwS|ojQ7`޿=€:oշip G Dq)d_<6:3~"({xR}zayNUJ]x>n ^LoF$BzXc w'cGRK:Mcb,/2 u5jTfm# D*SM&v!bܔ_:lpY–g-Pet]ƞv}}M=!g3y~g*#t'o @~r#skpݱ 7`'M,PτeF#K|`w6#"E?STZ~0߱勞̫JYa#(*D_$m"};:f4ߵ)jmm㭽}})ڤ)zE1}m4u/yS/?XLi[iϲǡͳi8}}f" _b8!ϸ`>s3Fkb'RէN}zNarb\q2e}P^!W`)jo-,:c,/T3G'΋ˀ/+?֓t.;rn 4z8{A4 Dr e=%5M7OEʆ;{)!_Xm՜ۢؒΧXBP!tj7ݖY5\a%g Sk %=H*-?W=2k:Vuo?{xFԵa[5qQ )F>y/F=-^H)S a]#y2){67\bS>W5[}O㊳M l`K=ČX>%Y3{?ˁ'SAhF7O^f4r[M!|a?̱nE'Q*wGߛ- %U? 3ۮGm^R[I N@~Ng6Wd31SWu!gȊa?QZ=LםfYzs|A[[ySxƦ=1W^GwvOCښ9㿱1nkv ;k(zx2RE0v)öċ}gxڦ.aDjw| T/X^2SR:n^+߅csgrˏ*E[ttܘa]l՘~,V闶1ӓr0nZ~??glȫMru嫯?1c8h-l&CLe&Oց~蕖nTʧbC׆NexBŽ~X7]y2N|m5Z}ܶF"o6צM1[{4m2=}v쿴5Sfxˆl igHQ} y~-C[[inpQٔ^1=6ŸLn{kz;bl:5GD򩧟<f/lh>û&Jx+ X}ڻG.9d7#G#Jl񟵏xе1!{˫ZmpY4G>|pl:3n:`k?zg}^A  eem3OB0zޛm:^ec]hdv10잳+ٴ?z?ې.v2ѻ?wV'0F9 Xqr1٘{M;=muks㷦V^/hGL9/?^;/Cߛ]1c|\?RLxse Kxq/|% ɏ%{wmc.PO^u*EB{k1e|&ta &{^9Ѧ/\ldk5s-ے~?*~Hqƾ  1}A1#cǎcft]|f w/ӾQoH^~kQ/O-\w]KٚN;k/j,Vy/˶}-ӧˮ6Ojt#<؋k$TI'g412!y%=S7yyUeެ&Գ6qʦX^Ye#:Ag#5CRry,1C׋,H8'g`zaw'ۈeٰ#M|t%MPy.PFV^|/& 6LJef7Qk$M̗1'|qtvkZq_&S1jD׆p2L"th`X8/ʐ6`m|vT<MvipZ[{_צjGPcc#\ {:/Z8nô 8.4|X\^|ٰZjpε.ϑsBZ\<4c.=<\sagym)~F܀kmݙ{ ll"[9մŌ CGiĶoE;m7X 0R{A_4f>誡~}Niuڰ暧 ?bM/G)ø;m6$ʶ;e)apfC [Bx{Y }>GxὉUzdm`A vCl;rCN} wd/W֯R} ,_BSO7cmzl;jьݐ-#axtL|~)s]d9;g0m0meLs}EM^;LXSYjeæKȱѕF.k˲O% ڣȘoaU9|Z唟ֆamGj{r_УCF.;lp}f6ڷMǣyƋSg c8vb}nnCwzNċ6 $'v)_:N]kGҔ@ L˔' m˼ΨY1Fo).@]55+]̦0?8C6ryw&&]3ٮC@i%:*G32KP u*ٴl(;OE=´N;|Ai#ǽ nvsӧ}AK@βL9rd2te;o9 @O,Zgkw;~Ԇ?P0qrSqJkfӿw;!a57smmUuU鏷Z_oA`?'?\?D|iwl4ꚠUCnk!S xVˇμ?$Ѧ_+3%+IQw<X0yHx-{e!PuBE1rY`2L2~]?ܟYBr7ChFG|1+4"!k$#;q:QʉZ?:ik¯k߬g5|]ivHw}7 67 Ib\/H;a.ÒK.Ko!6g}m+ll_ݹ/ns|Sޝ7,9 ax,u\ ~c 2-ck>5c+d35Wx Z==~=cf ?ZfMө7y"xB_NˊNrY"yl7woߘ<3eKbY|)_|@/ʪ*Ń"hV3G5r16_h{]->z:~7l'>Fwm3-ϐx "Y_|zzױޗqu)GndUm)ZW_z ip ӯ+Hy*27@̤wc%=ji@Z_~pqDž{7CEFrqt y׆ֲQ aꫝX>3~.VYeB 9u {FX7 i0`VbvJ7|UTT&MK^UzvϘb0G@*S߼=5]X|%2*ьۥ/OŔ_3~y?OmXIϺyT8CuƏQOs~7>>]0%q#ѳd)e Mby/Ȯʲ>Hdl|+I%j rP坱-Y^ɶ\UNbJ q}Y:Lcg[EAn\ ++XoV?^EՉyofT^veY ɦ=/?N;4'ؙgy~#ߤg?S?9`VS%N1˾k?餓TKr33row!!u[~ƯO-?\و-jm:!#KQ֏Ge?%zO??ᩒTr>epqM@&l^5+Vتʖտx 8XG6$+[ 6'{J[ۯ*W$Az;,p >mt6 [oOEV/QkJҴtS?k%sϰ{<)đk19e!ȷz˧bdZK,;β0E"{YPۆ9ývzmv\_ƎՏo:f +ZH:eptvtS~47kxk 0Rv㟧$Dc-,~Ӌb8cv%ѳ[qNhY@>;Y߱)ژ0乣| fϝ8OClPCb5oKv_Iꕥi:1F2qY|]e'KI\FI_1Ē5r>~焬:%4;Sg,OLj5ʽtP{V[ՈuӮRs5o3nܸ eW~‘G*Os c/L^暾Vb(NymXԴ٧_h\| п|'?'>MwigO;.n}P/p{%HTo)}.==SVȣ"iisŘo1^>ᒦbL}6M [^?bՏ7y lPm,HQnb= +<;j[qpY6fmC#}#Տj5cM\k]!Lo >On* Rrie[K f%@깗va)GzEMªY,"PTVoK Oި&-đr~U1m#.kWaTm"d镔: QѪHHR&.^m B<*K s(ᯕP 4mv9]jU䂵".{. bЩ]giߖ":Ǜ/F+u5_nUUqcunO[THVG!ӑlZ}}7WyGsOrc=r[ϻ:J+9fV){ xnNjfB>Wի'XIHH%DióMp܀nYo-"K}'_Ǽҟ /"؅kK*/ߛ%F'`+C-]V6ZK*5@&KXkz28 Pmi{U[~qbduJTfN䕒Vd솢Z\,&?~QR<&ZԀDOvakv%_꫼֎ [*n޲AJ+V=mRzu땠< 5;(%[j+z +q~RAi@@ض/'Tگ>)]+.j+wM]?(XRz]Ӧg"{фveWkw pݩٙ@O/ wq uOPș0-lq&@-V^ȵPV~jz.$iݱwx{/lW|v̖<~AaT FZI%v{s/okahĦ\M4RoZ4_iՅ|WtRYQ,IX:\KRwapg?߯!U]:ubz .׿|?O}sz>pX~C; xgG;вi'~a_ u]6v< mf iU\o2o)T?iv+?~®Xs{i=渰Ԕ?vO&bylح}e*^KʆM`'+k훠mrV4 siF{}pAjM_O~t(:-hiWrkzK|g(꺳ko~Oi\^^ۋs? X24L B>[T3®-f}ɂD U6/L43]}=տnTg\n#Yճ2dHO=뮰^{9dl2 a֋.ȉ#i!u;^+At4E3Ktmΰx-Y =QGX_ɨ#Wdhg|]`>̎WXbҧFc1OiE{~z=,lOl}ig IDAT8|uwW/e^[>^?r~#mkViGgGgg+%@x'{tB݉Z|A$pM^YsSLo  L%@gK>!ph$u^dn2vN<3:C.i3:ˇ% v8?){} >7vg=c*s-y#N;Ēi:˱+l#,3_qݨ,_i;doaL>2宏XԮy#]TIO abigc8zZVۈ鷱ӽN&=AR|G|T,*1^~M.'W/?;^Ư)iFԲÁ7B\.IXʓtRrw6SԷˆ?QICXw)RK-䓩s=\b !5/bx|mDꫯ^Eyڨ~p]X9BE[H2kvs);xC.D)`EײS5_ˋkaMZ>]k,MަAoF/UM^xlb;ɕ?k0)tjd&M,hIՌ{mXaUO~GyT=i;,fI{m{zc_<q3~M?0k_fZt(& U=Lg@E;HS/8b^UmNK%dJ' ʋsYT6x4h9lpzS;e?e|0B@_gS:^?鑧v']UV4bg)mO{]o|?N9?e7\UU/ ]8:aaG-׾VeA+~w&3z9~1.>qүx8ud7%7|VD*yz칈g[==YNR`_I"#bR#MǏQ≂T$ :ŤQY*KQ=*Wb%22U&;BJVvMU=|@./,83w/4x-[=Lc6|A@!S_lk>yҴZ!>4d:{=MEvvW^ 'tO|˝w#B y/_`WjbU&thLI: ? < 91#~IV[۝'N*Zƽ5XqbR?3Zzk=GRLH٫ f~5?ǥSӿ-E*C(NsFԳ/ٕ: bf1jxkd3}Qߝ|>`䒇w B8s"D pj&|(DjȳI njtMqe]c",MrAM} x!+8w䓆qLu95{qcGibM47pO0`vB|+ ]9s!C 30c-/ 3-TDE'i|V{aLJ1*+5y,XrJORc=ՕJ6R-;xj[]uRgRߤ6ND*AGW<{Qko_J R{6j#h_e_ )66 $Hew?\' t_/]$$(O$z($ (ӎYg4r$toV[U|I^:]B P!G4`; /ہGy$\yF=M)L)Lċ%D  фԒNB\\rI'vc"G]d{9ǐjHe܎F&-`9L;\BD{:gj?rH8sؕ`BHu=" fpa˻ N7M{ Q]o|?qɹI'b}_,; Z̃'sfq6 E⼬ӸhhX5!4hwMt`~w>TՁcƌ}:SVQ~Z? ͇~%mKciBXH{ wxiUG +AG,vC&”acɘPc2NɌ@F #egJ! ^&fǯn1z BYV$x )RYiKZ& Y@"Ŋt璖I¤ؤHeG_47<袋wSA]tѰ;GkԎ~=0ݕkA7}=ztHHJ+^L:۱}? dsev$bIl}U:vcY EO>d8Sÿo^r-6pCϥt&d{uu~JYڥ*b D*߬ ᖵnyAȉ_眗d2Fd2yg_a‡V96aR#WAH%PӾv&tkCzz=8a[guŃwWCw66lyW;?}]n2)ѡͬ7AexwPl^zi'LEiTUN'z.od׳>we+[o=Οg~} s61뮻Χs%zD5/2|ʸ1Dv8#d2&!A9e ˑ‚{yw|8g>>TCˑ&˦ sN?t f7SIׯ_xvp?Vfr~%nawB4xؔ'gq?kfɎ)cɴQluH D_d,ym2 U1SdXb Fb٘|yDo"I}X K:FDČ@F #L|&v]++H_Wfr(WʖƲV7ECQKAzQ+:m֧xjGutd6Ed&?H&Ʉzokyrn}"4T_+BXl!BܻzHN7MKtUr^%rʸq-,z:Kh/>vFxw|a,u7z*o՘v?H$1ֻȦVWnWF #d& 7}#JH˔mZdhZ^KʯA36!,'phhj?Ytwdk 駟v A^]vD7G;F=|b*i5U\[% 1bf?Bo)L˅x2MrvųQ`';Dz/+O߾};R(7&4lu_ "/L3@F #3 vǐH6.4MӞ4l UY1]#5vO竑kՋ"[FY(Ʋ,On~UW]K& رcûLEw I2n򐉃dLWHvrRC^Iv{G;~g<nraLR\Tƭ܆@F #L@$k_{9b[xy3~=?6ZT;SO=~?O c=͝'IQ:5c (!,,xbssdߏ-zp;F 4q xQ~/;ք z vU9Җ}Ege]wCu&k/*y@F #4'<@5СHz㼬[Ҽ?-E*{ҭ>.tx8iwViQiyc颬 ^L2uu G@_O^ZYᄏyYg#rA>`K'&6UFȲ?,Ǥ~OJ}d2D$,$"^-TKeUиj4~ib(IAC?PSW^ GuOSɚ=S:וk Ue2ɦmT~_rd2s,X`P^g?)dWYv~%T7jG,ϵkmnG qTʣ>k,YMU{Kf__K'+햭g2@F #=C =8Sl|d#VzY?׬6q5QFI'O%`i z$^JfUUCy 9#7}^.Qq#ڃgqzqI:9f/&E>-TE;?Fmq'#d2@wg"8G\oD^5Kf.<9h5+j)R)U02u\2WZiĉMyqL6;퉯UO8-fű}?S[o9P#d+.U!_U߁.wqNveJ,5L{CF #d2!tWM٬?iY>Xk]e,#լ_c+cM -G*yplASbUv\tEC߾}f @Lm 郑%R +tq;SP3:wV; J=Ӆ;ӖV]]1b{"!K<Alj|Чd<md$g2@F #1!{)cAj$ў($]gE M]Y&Ɣo;~*MrR>cƌq?x&!͕W^׿}G@ʚQXdE|cխre*<٤Lb9ّ:,K >}8"_*Wzd?U{'{qܨlܸq~BO6:(g.:fMl+[nN;md#l@F #d@nT֎\11lld3rFˑJon?&g s=H"SY;Oq9rH߀;"7<@x գXDC:CFEyǒU,YҼZi/?q>ױllKzSZvX22!bK/tx|!,ځ=!t:,c)GhʴTx1=h2해!a%RfJ5X`s=>8e9b%@F #d2E@KQ!"N5q0ޚZTd@ lA83[Kzw`zC=n&'z8s9+7|^P@ڐ%rK8v|Ҳuj#'-C^!-Kȩt\ZhA}RlƱXK 3=!x o< /d>_j&(;2eo(‹sذas- _|wqκTsd2@FG3JAD -˒$4$!y$3~?dRR$AȔѣGy4MT x wu uQCqob{O>{<5խ8WqyUSH4rqSTȭ_믿qf5lյ=\ yr0v&@D36ҊȲqE+Xδ!d2@F #ωNMBxCN}zn?pWz.Ih 6EfC8dyԥM_D`ҺKr4Be#chxC$A1Ï n`x4h svluHt ?xF[Xzr9&rd2@F #cSծK)H Y _?H"$";BʘIAþ"0G 7;6!@43% !L}CxAz ^p<62 IDATy4NtzeUqT"GXgTVZ4q~v^q$FSi_uUsСN>!xq=X`􅩵K(zX15ÇC h2@F #4 Y%Q0EOIELtݒb gz2~|6OKJTʔ[n%l馝16 !w}\w}NLM?UUPXe5r h3O@* Tٖt$*JH98 U6a-I6Nc駐Jʩ $͋ky<ᤓN Dj*dGn߾}M>c vS\#/r-#h 舔uBBxz*6law};f1ˆ,d}<^6Ok}#'d +/qF #d2"`bHX<]u/Pţ3iTKy*$uꩧ_~9 iDnDd`k,$r 8H% /8UIsZ2JKWʕ&ibgABtRK6đX7^yyڅNZuTw\Q{X2lH$vu;6›w5ЌD6e"o5ㇵ`\ӥ^:l6׭v8#d2@SHbm1 |{njDL&e ?RRҁjߝ(Tt[k/'.l yb)V]q;ϔ\w'e3N8!zNbVvfU]qX_)'/FtZ(=+8~ŔŶ&X~ 9UJœɏTuYc]O{d2ހHug2@F #1r%kA uIYM6ʬՔ,Y԰>q^ G*c"":x IvLDix㥗^ mGϘT7鉜ui6Ym|'Rv%e-v$됎zu+U۪ꨲ]%Hy{|*q]s x9&"LLUP$'T7Iө*G駐b>}Ֆ~EPvye'VwyS eLc<^F~t`:,sĈNƙ.@֘P\2.d2@F #dM$YD"{!?p}'߾}Mlj:nCX!)fUW^3@F #og:xxs^YV)AkRRw aFtLEUWuALjhlĄ4-8MIӴU&CO?)ArO$d;njxuF=8A ؆4L^b+đ );M{ Gq<=x!S}"iUWnld2@F #y`F$%1nYy:u_wO\K3P3' z^5w~x;|PC-KZu!?k$Q6qa׮J#Hqt-Cu/۩ ^Vbd~9ċ5G0]tr )a[/?itSFsYF #d2b3"EI2; _ZEPd;>?`)\!]Lw$@.o>kh-,yAǫ^*^L,dSEJ~Uk+;sּyUri3͓^7;LvP~4#@  O3O5gI2Օ))g2@F #KkEYwXFT.g2~i)R [NJD@H6a–[nYۧXl+꺪<#I31`}ګtUSN'Ʋ~l;+J=)xXb MfM9d2@F #php.jHG;kZS%[''}M -E*,:|gӞeY.qՃ@NBL( R2oܒK.gjIٌ~;d6a:n?.aS6+@F #dZ'rx#!KUR'Q~8뗷>c)iRRRr9ꫯZ:!&\s?f}v'`L%eWz!ևWW6 5lH^\OUv\p{٨gvp)N?t'*lR8]Uogb;:#d2@ܔZ,gsPdtF I˲LeS*Ljifhz SA?c=V;V yA$F';V X?ˋlLױ-q=N`(ozʏ_K;lK7M. O%dž|ҡz_+˺Sd'Xbm&ӞYKtz_U.YcNi׿9d2@F@bȥvsr&SU׼?-E*ygWPuf׮O?=vMIHOGIN!;4LՑ~G[/VXJ+SI[nVl#Njkʥ'{i3 /_~NZY لtF #d2!P!{ec;mqM$\$WΌ /PKuEЅZ(,袾+ /{Tl曳@:wαq\^7X`CvRIki&81Gb9ywyhWzWG=yQF` b93;c=|J+R]ź2d?f]w _|q8SÇ~إ-9d2@F #5J¨xP,{e~;+t/o;x2~]?mH%I%0eN}Lhs9.ddZj)_;ڼØĤc<Ky䑰HA!=BƙBJ@/ ʋדa)7*-J+$rY7O<_K-X?#ɿ{-6,~ Ǖ{@F #dV 1P/P|&K˔^͚L\L1&2XpےMWF /o馛.?) jlwq\pU0 1rTddG嫜O:k4Ȏ\iÔ[o%JOyĺ3E ސ8@8clːvॼKJBV!{]2nz:5rT~CÇ? Bsd2@F #!)Ek{+:Y02~Sش-I*P-2aMU:ʅi")뭷ot<9d2@F #+Y'c6bݚ;ޡNnqD~@(9~7쳏oñQ4خ+GƱ"OUWb[ʃPO6afg@ǔW69YS/ndSe2B! y]|?ϰ;NjzxygrrS_ىz ڥ<Uz/2VWU۷{0Ǟ{il8#d2@hGp[: ,2VK@=׳ -E*A. ^+vCe%S8Y3 [ƔI[BƆ zLOn*+b餱t4?MK.c5\aM6Z,)Kr09s}0$?M(6K>]kΕ\zQfUlf٘ߜ\r QVQ^Uy6r2ݕK8@F #dz363ZFd~tm5;B֯AEƯ 6_׸WLd=%^CxϠ" Ц< d@S`!_z] tT\zA!g1Qe묳Q6ͬSăTR,Cndv{TY:Ǿ}.,09>{Ny\=cݣJϺdS~2@F #4mó4XWn#~ƯܐqMRʪ^A XBXkn⍃drS* ;s:9|B*/TUōtȣ/Ueqc-$}ɫgC2J|N!N`%%*;i#mcZ2D O(Sa!x LW>|vgmE_X qa~?0=s9y joU_t`/vm[oxr]v`.|Г]qF #d2#` Rı(,.BHd~ƯYSCK{OLrÉ'Kɚky!8`]"@Xh6 q]uCIu:\sM8c0/ NPe]$ LWY$R}kY'FHlÙq{'\wu?{Fr(1cƸ˺Q^xK.N*̇^x&c4齣=؇\pN~P8p`Xd`68PP\?0ٍKl#_g2@F #h",kL-IfZq({9`meڰh4~tS} 5K=}'-A(x1b 70L34 +,^L VJ.'Y|1ɰ-CG_S<:S7[ $m(h;2I'9u]ŴXfC{Y./y{\5m9{&wu{"٘7p~cz^~6&Gpf%d3qυS3@F # `Or8vWR^Lv_ _CDWOr-G*H!!6a  Æ4x $Ld%^M&pf6,#&?5ܚsR^DlD~:O&d(2͑iWGAH#_ӏYJH?|RY۔L}bZHD (CM67 њY0n}@b}^xb(!?nG|d2@F@$qP[b3~ ݘ~HeJ6}=*cU <_iPi>LuUUŃ:GJB]xᨣrRZ?YB [Ls !󦩜x Yt#z9dT3)6 i<3ʀ,QŸ21]|v 6AJj+LLKg:wN՝@F #d2]B@7000|DsZ`)Niḧ́i>2OgLu{j0Cq3{rYgEe8S?=}Xv>e&$K\/dž:,=#qWvDHk6;)^qW}sύ%կʕW^6#(>mqoh+Oau~UVD$bcX+yI'?:| St>7000EAKC*Y8uj:Y&DZW;ʑܞ\MO9ıX_(G]tQg:*Y5b +[v+%Gw]L[rWeg-y#䨝ژ}iu5u0.6Ͼ0s8mo忧~zXny6%G=u"2r o&`&`&`@r \)}No~g9Q9έB厥lds '.X iSqE(qyHvyu"lڃuMx0^կ._~QDu+ꩧK`vi[.첂Ȝĉ1uni%sN/}Ka}+^QKm>000C@K""h=ُ']8C'; nG'=GC.^H'gn먻M}-K.\pa hsXnX:Ӆ)4&˗/K$X"wzǎlRyOXȄ5믏(k-HE}rKXl<2aiy‰sڍ1; YGf|_>xצ>GRm>Ly'`O\,|m&`&`&`[z~$U{}_ O#D"}77?%%*EMozSlX%f)ֺO>y/{춊@\=/"K,ź 6%sG<%d3,#]dy&ȃuC!DxJA"`F:/zыʊ+"|XygH9F^~0޷ɫUj;F3<3v=lRdg&`&`&`C/C:h&H+s t$K9F]m0# " α!~* 1:6ݒ0#ẕQ6Qr]mt i˖-vZ%;i{]2iaxC)g]?:gO5~%J,/"+Nu͇xu oCeʯ ?O%’0>7 no~!sP:nLLL6 Mɡ{ۨg!M&qӓ{|6aП:tS%*5Ia?^/~1K@1l!od%2:,dM,stP( =sᇇ/AkEx&~}B]w5|sb {~gP>D$>JCbĺywƮP+\sM[ e^zib-8͖|̌yN(q嵯}mB#-Ј8D-r-2000 E@(6?*1_| 59{"43[3^TJMwv (Wq Dx` d3Dwبuɮ,qul$uZt"]Oyf|XҊ%k/r^>EVIHMNC,8i^gBD!0!"I7tS~Hba}^o}kX!}\2ϩrժU!YReK`?{7Y[3Pm8i;LLLZ dlS!,)gGοUf3|tڕNRQ;SyK^ƫ*C< 3zKĎӟBD!Q'&˰ofѷP.lFA),XYŠ;*k#o>B!t-/?Uyf-ھ= YvEN9XDcmu!X$N@[ kʡ- x,&`&`&`&06VIbSRX$2t\Gk7?; 3#*8MkqP_m~zdT6Bh$!P8ĩ -\#]{.H6WU~r[ng>KIy#8Y|'ρ"|U0_g9Wx?*uXrYf, 1pyncm$j6=XooCzn3W6]7000J:I PYLs Ϯ7m&}&w-侶LkmAr$Ԕ7S|Dа+GJA#euR&cbD 6;,^夓NG6a +b<>u8;SިzTS"1S֬YaXG"̱\J$Iv=_׃L,zԑ۟ j∷300099oZJq4NUi=NZ @c~ ?=ZJQ'6-rI%:u!3h">OիWDSYlя~4,x"r?TWbg +G 0 *Z(^~K֠:}}]gjg!6IˮX"MؕQ`~߇}Ӟ yv|mb)>3~I움 !ބ ظ6]kR9 &?ؿS%*%4uD@[x[m+[mR:*XLqկ~5U١,, . vxcT,x,!9űe/ !Fڬ:OMO,d##뷿/cG[8.!M"6ٹ]vJ4[I/:&\)_움 f#?)\SklNJY%uCo~?tT(qㄷm S[PzyG\s)nH6Ayyޒ")%lh*|+csJ'd'.,De\]O}*.0au4y[DX9K9-?uOľ{ۢv,hЇbc.oom~ *RIg5,Ej6a,K`O8X"V8JT& XKr9vb->mG•N훀 @gMv-وF@;6KeS&Bm淘YnK鰬-Z"@"Fׄ)0<㈋P:krgo96Y~}y˜nuN*[u\3 s^[)OW:&`&`&`&0&!xqk±LZGH?W:2 O_uM~#M_ZH|*gXa*Pg?|[*,=scC+W?я& 2Q69SʺugQ>Ov@=Vd6m%^I\Bs^Tǰkա6zߖGULLLL`,MϪ gu^W$;M>'9aiO}jlҗxl$‘8Y&9䐂мc68 yOwaJv6KTʒ_Vx:2sχm]VLLLL`$ %&Yʦ0H*CqnYgd/;rEw3cx6qy;,+%H>ȳ'b,`=+%xnn? ÄV[ڮ†9,z] lq!~ZCLV"Q1IdERtSG|oCM[5>nRѡrTnG=*_.\ۭ2YΊXxsE.F^xa/r-eSO-3S9\=V뺎Wx.;iKaq9׫W\VNiPZ:0jM(H\"8?^NE#͇6cf FE' u!&5Иl&`&`K@ a2[N4Ο@5|o=?I'=Ieڵeꬑ.VcL1p9LLL%Z=J46FCل Dw?tOs2Ԁwm?Ѣr6`,=)O/5\S<l8$(wzC600<,_d=ڷ\uMšz~m~4Q}IR_7.]ײ8Rԇ{WXTj0ELjҋe]BP~-z98 }媫*sNyWď5FcFh&`&`&3sd_u1?[S߿I^C DO8xn72?n-yk_j^OşmwF(pcRz=Pam. YիWO?~ OxB4ѩrƩiM`` n|.|VVZUvu;ǁ|c?/c&`&`Hũ_~p0<ܼ'd vٱlۄ7*9sڜğ8&is>C3e}g(# }%s°\恏go}&`&`&HP"$91w]1;֖lxs}$.O`-{[c}+?1|ڀ!8s̉SI 1Z-}Ŋ„į48h kydX"3? ,#{_߆ , w< t&*iL2&4uFBr܏glpLN>,pMLL`;:O%(|b3i\j=ѽssL斚_/3s"QI1bi 2 JIPr3 <$*̣\#С/cD15S^R;rwLLL`"w3ŹGcK:17R[5m1t,_gsL-5W]JU`pऎM4YTj|*۾ ,U||TyLLL`5\WBv껟|Rj<7{r[fCCRF |2Z~R78qPsJa 2Ɔ~b25،:GTj? f&`&`D@y,>{]+ųOO[57|mAI?dh~!_=xkmf@jƍ01_ WƔSYMLL@|/Q) e;|8|4GW?f#J<wJY8dhh5hM8 /J'1&1Fp b5%03 f%iq_L#3VzNlQY?%W<d>h皓BYRMjnk~}р!IoL` hNu¹pSΕg300|w1'L̅57yn,!y(6T%$nv֙$wʣb K8fACɞO*Χ&d h2x5f?k;000 9潚KDZ0;|aL1I3LT!(/0R 9`<)n)0= F@<7[[<9&\sL34iIRрh(uy^R#]`VX[:MLLfZ<Jg%6BLٖVqMT֍o_kUYOLLL`4z[_XK`|rTܒ brf9 ,)&`&`&`&`&`&0,*qLLLLLL`XT§6 200000Y `Q9 h&`&`&`&`&`SJrJ?7LLLLLfE,|Jn L))`,00000)&`&`&`&`&`&0,*qLLLLLL`?S:\>IENDB`amqp-1.8.0/docs/diagrams/redhat/0000755000004100000410000000000013321132064016467 5ustar www-datawww-dataamqp-1.8.0/docs/diagrams/redhat/direct_exchange.png0000644000004100000410000010133413321132064022313 0ustar www-datawww-dataPNG  IHDRW3bKGD pHYs B(xtIME ;ʧ IDATx^eEvazr03 CAAA@utuhZ#(AEs0SOw}ogjsԩs7[fGpGpGpGpGpGpGpGpGpGpGpG= Pi#vbʗvVrtT9Ow_nVkVo\EǚtoJa#n7+e6xإ^#8C"Eĥ^zkXtn86:]N|gٞ cNTJWݽfOʫ=yꬎ"c% O;n~J:)Jߒ¶vɞGpzn^\{ /*mo5+]Ej} Gͽs/W64k[H: 黒{(>dm"ItTug)/* ΄t=O哕רSݣcx[EUߛ3Sg=GNUf2tүP" WEm2gi~| ";1b-"3_m?WU~MId{r+s9&rz}O7J@7Fei;uyjGa¹ڮ=kAMI{ԞTx}N|ZDSue )E{cS2kmPH<#Yuu_Rv^l#8@2xEi:[{~](ut5w~&ywU^ ! N^_XImĪ=^G=My2jAJW%5]O5u* sun=!ٹZd/7dD[::]}g+cW˕ >-Wi}n}j C"eKUm?SAFa1*] nbǕŻAh?ÔI^Yf[;G>|~{8@Z @$ItH ;\+ ?G9cT)MIǾV[$?NnY;"iuKZ![|V'j2fi{F…`oܭv,o?k MQ~!3^%;oj xʨ]R>S~ǫd[Tѡl[*mG?G{uv{rG Hᖑ P QwBٟGYTitxLG'Dӯ]->r^cZ{Ue\q8E=]bR z^ ++CJO*QA6/ڌo}{0QCF)KDo*L7 A!Z"Ѕ9MTƛBqt.>RßР>5\AԪFʝ=rOeTt f2Rk3πcԮ5ڶ;tG`@"R»INK:W2ލtKX!; 鿑`#8.v2*:^ L6H.sхԑCMH7DKճkꡙE%a>SF‚ŗr\qm "=䐰SnFA[c)8WbqҖ*!:MՏJ|T9ok4)OTJJRJE3 @ǹa3^-^wOG̀ U$ :K:_ltS"{7R/ǯ$ړ*z]nd9Qi:e̙ÛWrZ"%H;deJm a`}#2g [jR1C!WCe8Ql ޣ46ϡXqb3R3@%3OKU]POG$Cxѥ"$<i:Je'V[k-XF# :zVv^r"㒷$|{MTTO<G%f!C @8o"1dX&c(pf6IA 8CCHvktk`}e$<1UfC a*86g#i(W{8{5&r^p>C \`!nD~**&}%tCPCe^?^oHQ9)o uw? O(cC-٬*M=ADq݁mspWL@C!p&31]GPJyīGԐر&H8ܮ>O[8@xE]BPGwQGa:P~Tۈ׵,i#'8a4Vx9Gp^:A:q:P*L:qlB)2e6&qpugvwⶉ)o>hx Bj7fջY"-r  < |!d4{ D7yy^Su$F_*# zv]Xb#+3s_ a*B ԬZxsyHxr/PT<@J'\WFr>_RHbԵ[T)z]m9V/;U=QCGs[keܸ'FM|({rtCۄJK7%jju%|LRI&umDҀ h%+R;ikOkרrpxN goRWHULak:*oWǵ]KX6#z.:wTު}7RgJ<{Dp+{Ysv~@ʑ ۗLeDpY}H4]@G/4SO}2{(ߙ>0Iz2 {t!@%!Ϡ]IgLG!u5ʄDc%s9 78 z-~EEG\P&\} rMԡf8#8#8#8#8#8#8#8#8#8#8#oz4-ᓟ$."$eiߪ Gp @t'RR&o1Z:[uDtLT'0;L|LȎuֺUWZGp*fB%#NGS$%c(A,vpGKXE))&lNۓPh1J$+@B%~-Gp!/VF^yUwݍպY8'Dr#8G?Ob'$ǏM6r̴ &!bvڵkȯ}I1VXX']̹].tȣF/~ziuGb9MLw6&rٳgO;;v찊 zrB~Jg}vϟo&M#Җ 3fwF\<uQQ䋝IsѡYfرcp;Q& 9R uq6rH $yG:\728_DlC7w# /%1>c!@LWOi,ڷyf[n]~v'ۢE쨣 j?OA{%FKc\C NO?t8ԩt!h6>4?a}`Tƍׇ4W\͓#$ӧۗ%5T.%P6JHS$<dLDM侼#0BNʖ-[42x~OC^S /2$E F /JѾHcH^)''g/t")|hTk0zΣN\l_s<3{'7'Խ^Sc}ߴAlb_2lI9}37d7 >p Y Uœ c/!MA\!h*eH>9 T5؇$G@QB"-qvw;묳HBYUdC@ NYVs*<k*Iѷ}c&A34P֓s>"Jؼ}`W;S7TC8@N<g~SvAկ~5;#I>ēWp>EI7x#ڎIWN=Y('G lw}wA$+x2M}HRMx3u]Ow??sx?F) *o%\]z(n]'xIwte+v=n{w4,8t<vEo ' ohlߴ3>= 1;p{`0 & #muCVǾFaHjVdeE#,++ hJ? '|4| D[( i6:s熏pȐ%*N>˗ْ:N}Jx! x HEx!-T_}{~>φNbъ` BBaЇC3Cc0}{C> )9|˨O@x.~aI+= ^QfqD(V ?kg)"yY<9]!I r2 dB&voԊ7AC"?'(Gή]H|QQ ?rv3xcW"N8!Hb}DpR7D \Q' 2 7a$0h2[woBF:(!/.Ni8`g*kP/dѠ2zLC" Sow7_~^'DT+WuU׈OM< ?w"@vdBT'N j4 Oi>!>wFLBOݜ@DzHmhA$"r>C uc!7TH+ć'$O0D oEvD$ 7,Qzms :D9^ @)Ds*@Z ֝a (?BhU"]|:9l u+GQqw R^|B3|UH84 E ww!!As9'ޏc[tt ؂!EyHU/sEQEҁ #ɍD1lkH[HaبG )ԃT9a~M7{;SOӁ Rv WɞB 81P1RF+~0N#9BdbL ) _C4#@zزxv.DoO=T LMTH[䐸LyHۿ[{+ǑpuFҀ:&';&W}r%??B"$ԏԉxi"a%>Ay{oXH!$r0_s}6|ܱ7p| :s\M!+# \O1HSGO@C%D=#8KxW--$b$Q ߥ}rdɐJ~&`I-ycRYeK3xq[ɽӲd˔8 's@`1l}@N33#6I$[l"@r8L+# yvfkAq<5al{% ([Q[j9ŎKNS"^?#6Idõ* -%"jRG29V4F6k\s:,#PēMd^2SWf#e!/LO@: Ot$ut'ux0W$j,0*Z .+6QA]k&M:׿Za H)yn dQHl+f]86ND8Y+!mrX5O:INS ^ݱn4Ջ:}^sz9ӄ vp<ҒY)Tq\)D SDzJOnU7+D+rP@R͟?(ꀼ'VzRH|DHHHeYSLP鉂-[?>1Fnj9֢%%V!J`˖-6F`< vƈ.P $LxArs#ڎTRRor!Т*tJEy /X:-TX]%]DڹZӠERCuIT^!cA©[#=~úK :@qcř>zMAm혦=+N}N{: hR;2m+Ev+v[2En#Ke$ng8x #X`r~]uɫW7.?ep@|G/MU~]UyWvnKx u=yr^A-v|]GGwVۥ[qMZ5s/կNZhK^d!w`c7V܌)IIHeYNUU@NԝGhmj,5ݍ>//sGEE~ ‚C9Rۗ[ZNMnkuje|m<_׉KԐ5lkg*DlznWůt{.)i5 !Z7O+gh<odfe4F8;FB_sizLxh]M\O bo=g21_0M#{nm {W #(A L'OqGp 9qPQ6Ԙ Ӊ۰ #8Ds c\ԭ*Ex1"|Eɮ[p{aGp$s8hn[v)Nv cGpRE b0Ib0GC>.ukrGp\4'M /MUQf]Tr6E~5ƧEq7R:ac.Ǎ>k<՗Gpz.*oJr 4V2N&y6峟©AZ^˗l{1[_ߩ l-2ӭQ˺8%xGppt8j*t"ܪ.={,SR_V:wtLhK:$I2SˈdjIhm5鎀#8}\'Mo+h)Q#¡DNikɷe;FI|^rzlG iӬCq.KxΝsȭm…RKSVAi=0Ω[T +3NJg϶+Vj)LS#dZZeGe,ŋt#8@'RIxx[\:k”$Y VlzJ"YՈj-Qg۞|6 IL Qze["2rrsg "Q}K6$A][o&I.&\ym_YB韬fJkb8#pk.UJx k pOšY@p..,"$$IeH+2 Epo1HU"? ;@l+E|6_ao3m{ݫ8 NIu쥗ڨ .CB]㯸ª/m> a<]#r5bGp 2%JxYڒ(p5g&ĵ[jsmAjYg㪟6qGAdV4sֳDqK8d9p%M?I~Vz5a'h&H;y=6@OuGpeaoZR#UIGlEv}65.6;%+H"uflx(!? Nx96JY$wޱ*OC]JiQ7vwo~3>hs-op 5ng&O#8}qJ X8D`#Cs㢴N}PuJTQjTY7\#$:ZOvA R_3)lmuxv(%Uc;SmN)qF~kAGM QIT@qA [][ ^O.xOfȻXSP/ HdxW*ڛnm"oVv6N)&g9ic<>=9#P9 R %'8}%N+7n 5s {x#80/$/!H\IKyF6W_ O9*5ML:+1ÖyfzrGH&^%M*| $Qespa?#sDFw^:&]LQT ҨLk܈\ ON D{ DH60/fKG-uG`h!W[ZrO &{WIR[!Roo &#@VvH- % R ટ*5nӦം[_r2C RRbTև5k{˞8![Gp~'<2#D JL\t<|-*imHE~wu!9;$~/8#?$?M:#8C /uXzM#8'pi#8C /uXzM#8'pi#8C /uXzM#8'pi#8C /uXzM@ @pشD,HAi켡=GW&ghLZ.+,ȴ7tSdg]!L^lELvQօG Q&pM5F˵%zz"RJxY|$3ie)7rV/oÆpl @+dYSm?}5د<’mkmVr,[aDJUJ AhycX{N߆peYVf kyrC #7i٬ AxkKj_6W_C hilfknϿ`vk;ZU~]G] \dϟ#7+y{X^֯7Ԧ$Ʀ'G`!7ߊ#m4bޑLGԕ_?U>h,mtPkTUYٹZyr't~G daW^irXqּgU˞`KذK.H Gp?ʎA@)Sl?_[V{Igm%yNjr %N͛g3f̰iӦ3:$ofg?kYZko~c˖YƍWl1ts.}5~y`~n#s[/iMXu}߶뮻wA?DZBWF)5v~U>`Pq֯Xa+W,<`yӧ[GX 'Xe T֭[g<;*m^상ȕZ+S°AA˙ʝ[hF@ 8j75b˖-(vE8Jx$2E(..v+婫:1MRk^FlFsO$wy';!GHHb)@xH s> !D$"Xw) ;>Oٱso^jJ /G9O+#>Ikͼa궼jUgu!h'&X;>REj]ԻoN>A  {o_|qH08*E[jL(  ,"!:CYAo?lrsϑ"[ v8z!D2)B!T~:QBmߟ7HH:&B-y3aUΠڰf5nd-~9;adO$APB%:(_W7Re[}ӆfo +‡58L"+G+]kn9@Ǖ~zPz Ap"p@R8'? *G=lz9!AeѢEA2N$RPKmq2y3a#VJ[VkJe'A,z/R{M/;d !"MtA'G`0!E"#!aCq}{A'S3i{_FD{i=!EuWpa~j9VޮyR SQ.S/O}!VO?eD9>A~o18|k_s 'ԑ4J3THz:tȞytDYz&b b >׀HgqF4SOi/$Jۿ?? ғ#0R2U>Nԍjmhɴroկl'>a{ض{;1z"ܱY#ylx .l+†qO RPm81 "^ }?lWEHHHH}8ˠzxtb |71rF[# @3H LׁgFcDG{Zi?Uimx|Ң 3$0ȅ7$SG7qNT.QE2;F&nׄ\ڀ3 R%MA[uB "f9K/Aєyy\l'2(}lɎM)1]/l;דQAB("{#z`Q:9FQǟ]-R@ejшF$78@"TMsSv .S5#_IswY µ :p_}mmnYo< q62rCyjw#0}aJ6*"fC>xyN<+MV$6T ppqKz_*P2O#<^#8)CRS$uԛxnsSr8%8@I+4E9J+xJ'GH9%r^ b NbCB'!VG }@]V֮,zLUsz8@!Ox8 1MSp9v~#8UPk6k P^P`+ F &z,x@k8kx9GpxZJVk =bi&w鶳!!֗c(5KO]21V|M#oDAvsε*V( ͖w‹I TD̙3 ŕ;3m +.\YdxM7s'pmݺ5/ UI'۴"x≶x@MR>h N;-uBO?mk֬ L0]?#~mҥX=9#/c9#`&*VBfŜXnذV*4޾߷CxDEd|7RR!$3f_ M]p6GKj,X GywM6-zQx 3~SD2伣:*,R&6{lBp"/%d Ar%A쟸#8C}Jk IDAT!38#KMhޠ}r}t4,x]yyM8 (Tɼ<8 1z:Ԑ˖- ɍ;6+E N<9HHȕߔ.T'G Rv58LFDw睥8Gufojj4Hu^4םS /§MyR{:HlT2Nu[oCZ{/W_Tj/$,"OlqK,cʲs!4%ܬ͛f jԁ'ݽϱS[zyF<G o~kK|hnD x=7]P!E"23yzFLΈ0Z )FUP6x!l?` =*L&o|=8#b6Cݱݝ]Vm=jJKi :L"RKzQчCF8lCfG F%R߁Z 4R'`.6t}މvP*Gm3H *:Gl 8{CzoSޑR Zg\1Y}@H@ץHfKj,i;^wUH_bPZZT~ijڡª\UUgRk.@`۬vwVc^HE1e "fFgNyA$FI|'#8"ԁq۶mH*9|`t~+<yIq.% ,58}Wv1CCtWmDy643{I}KY2qԚ}ՆtK.$ b+/-=;o2Qw`;}GJJQ^)GUӠ2R4e.p$\2wBʜ*Rɒyq0։Ȥsu)V&zߍHz eٔI=_9+]N CNf6m ބ#zMY18AE?+`Pwsem&}>5ꊘzg2@Ku9]=#QhcpTyD^$m3?o:Ed۸՚n] [p H HމvـF4{6a )ť=+ѨQRcy"ݯ3eK g2@F`ϫ9MTk[%Yr+_Ծ laKs5w^IWdPqȇ򢠞ٲmʋ0%+*R#!1- y,e,e[-Gi!-{c'B(j`)kk d<غ'4zЖ!wJVVv[ ʜT[ b\+;ca> @' 'g8=B [*Ғy~R(J6!,bYRxq+1$ Ԣ@-23117ۨ1L[\6)S!YsjYYA*2| K˹TYR ]UӤ &hĜj87wTkSXB=o63A@*ۤ W[6mPشlMjo47I8eۓs`Rxmi@ ^EP6K* ԼGS)RkPdŧn wx'(;C /^Ei),_*WHZ!jg³sAôoZ 嫎t (Z?9kkJ7 $v mVk5rDϝk /Vp ae*:t׭ZsP$!!}eb˾3iR^">ZmrKDWcŪ^^ Z/HBz[U0=&ʠ5/d5(Wk4$\LOpR@Vi@*>ACM15D*)_VQ٣hM|;ER$<$R " dZIl]uO;t~T\ެY" 6Ys R/[b%6k{DxHn} D=Nx=tA@aÖFL̲c"BVkJXVB% ךcǢ,MEcN`SLIE좘ʴ[$lZORPi~8X`y~#r)_bG NS'֑PWXsIRPBtR=bûǖw%~CD]u .qɋ-b ;b}L٢9Po IAptUNqRiVןG``""67G_)e$[qAaZ mQ6qbFxX2 ,JƯ~5mҵ6azmBz$~n'ô#q`|Nx=9@R@$8Pډ7CqD4p(j(ū*z!ҋ. Q^ZRJ1G0Jh IԜ!YcǂWg&T섗fl 4w>'|2,K,Ϗ~a?F.se gե$4Bs>B x 6TgmG!>,:IO,٭H{MHcqg@HƎՃsvWVtsσZԑݚMZ_%'2 o>$ҫq<[dKcT,쟩) :WICj˛$qi{*_|1\j4y,ZuMB/o<sk<\$9HvIyJb˓iU`[םl;Ab]u{E#t('!e1\!u#Gs(ks #JQBEIK9@wcMIh'(&CcxM%{JL$ WgvGk'"JJJШQ3s 4_,J3R]'?*㩚 xE踟u"<@z5iRy ^s5 +PQ,{ '@ fێ#]5~:K_H<$_sp4# y{pGv3J +EAiTDZYA@2ԡUS@Lk{ں5,Tcu09@-S}ȗrO8M8g?Q4g?o[ EdW\a3VE.vߣ6k]B S "ڟtarK5^#8 "ФiՊ7[O.-S?XQPMZVhݧ?m uD-bM*QVV/uKԝ 8#zj%kzoHX_ =rYH Ű-RT-m**k/K 82T{J 'pR@RDCD2;݃3ֶTfcbZ҇EXuw[Yr6N+gj=`)&[jct ,_چUEZ{ -K9 ħ`LnKm!FY'efFNݫQnjKh)aZskVG( GDk]L\ܚ[naIyyH_XٶMsJ$Iƀ@kJ_O^O/(Nx}y.PݢγB ԑu%yy9r!N,ź tXHAzY3EPbrDR$/`)fك%[X}M镝l33B}D+5Zz6IZw`;#]59$Ex964t(priN㪪̮B} +T2ET 0*x^I¢|24SZ Eb6f"H҆sIIm{:MSO}}z~1lx9qnosVPж¸ mϙ"MGKR6mh96}"s0.* kpZl23 gᝥ=DRWVZb͝i͍ԀԪz|GClav"w7;))XccU%7:MyҼBlycG w:yVP`ؙA󱝖ZQn?77ۦOcy&!98m6bϒXz5HH[7A<T"J4.Gp$1' oH~ӎ# N~ SL&Gp@@@ONx yGpR@R^)iAUWZb͵-`p!@4썷ن[ A lv48}op@ޖ;GcGYYYmذ^[Uigvl ݕUR"/5gfV ۷=V\\h#G kTUXuM (ߵV#)"*1\D1u-&OV]B쾊ae"0Ӑ$6 @z-#~'cĎ;`;E.Aһ_sfg^u|q}Ɏ9`[(/R9bՆ0]bʤœ#8@z 7{T{ټ=5flggqle`lsUZ7mopò -EZbƴ VR\`?:/+T7!y:p-xuI &)RŁ; i{ƪsK[8@7 t$"|m<.xZB8|gZQQQ$6IށLdEj3gL N1]a$ī>r,!O[&Z%J[9@`#fYiYQp<Njl Xy2%=9#Nx@y,r'b,GRK>+򌬵<(7{T=־B%;89dlxmpM2&OGrp1"ůT}!3zKGpfC.:`cZA ‰R<)u :"Y:3 HPk8#G'i% &Hv<9#8"0/ƒ,d8#@O<8#Nx}_pGpG pG e#8@#8#8~ Gp~G 7``!1ZiC@pSFC0yS#ug 4 / Go(vz#%}k :*++GbNq o>.Ebmkz}vI!+.]jR4h;w:Xb.^ƌjHUI hhT= +766s.5(cEbL}vD:jQzpZXS%giꬲRˈك S>ЪxZ&Ϣtm{<\}En۶m1Z[P[$JK40իVU8мw2Y-[Tݰvɧ<-21aehTՙ|p N lǖ͖{ЬN;蝯{ lSyyeٽM7B>Be IDAT| ?3'Ɩj[7s۞SO僔Đ9/~ngylj*UE<-5\{X}#ȲKKm]3i|$Y6Nxp/2F*{u)Jffhс|ȹ,1@ a 3<7///H,>XB*7='"N* XX"Κn-n-\2a391N;ZɵZAwj;T^GBYaS'nN:)إPa LG#wq:^ħO`ZgW_]`guM2UyK|q/j}}m޼ٞ{Y{'N4 9 Ң”FwmX,d֯]2M5[v]V4rV[m'n;N䄗.Ojd;ɖN'??teRϸ 8:/_nwuSh]xRXd}1J\Ǐagu]zl-eNoH15Ԛlܸ`<}0١j1$[+cJ|ݏڸIl[DCx$-D]ImYlşzeKQo0Nx4ЉNȶln;,zwn*)4$WaZ4G2ͩ(93l>nY©P_IQ{N />KW__ʸqMBxmna\l$ =lP5V7ltD/ Q;R]ro'ud$khlгi&T"Lj=Oo2۶Ifs4f1OjR5RR5e'nNQ;!p:)E@ֆk^jnZ8]٧Xᵗ)IEJ/;*s Op>RZv^_Wokv~ ˚<~l|($G"iNNKU5ֲ}5?5Ze RdMkao{g8 7?p!s[v7߱ǞH:esܣ«/mgNx"8#[5ȭ֚:kٺݚƷZ$f +C5SK&imZS8%8Ciڀ&iwku5-_%[)/ִt(8ĻuF$בּ-,̓,gC^o'^+u~G8홟):DMߒƢi^O-$rne"8j%me- ּmm$)-35.gL=80)8RpUYYRPw+)E#D=ȫ~m@Z{c?6T55r(۶Z%ʭUAzL\YY CQ}BfʞO^Ww<YiXmִLo-UDp"7>mV)q!L_qL7P6;ڇ}ܸznph?ߐYjBf8mc%eV"7N#.;2?~/R6ܚWm\:r AӢD.G""ѶmHM KSG* *v[ yIP/O5~_XFLrkk+ѱBiP (}17$T[ּaUHXSeN1" -5\=2*D9fmaJjR4:\x.^#pSB%<*S2FY:q}#e@mxeb5m[&$~ZG0sTxr3Rdžcb_Hezy}RCi BΧxM -X(1dOGk;,gTa/8ߣ#Щ4.GGrm`^ք18>\>pe0C+lpp5pN' ndǽK'JxrʣOo]iuYYCQ;<(^_l5ym6BwQW[?""*** (*#<ұJcY|˨wGERDqD5 $!ɜM~L;Iw'vw[u+{_s: F AX.:>qm֠9q Tem$*xLm;b:4ou&FmlLz_D Uei8wa3d!mh#{C4?UAs_x?#QCVY#5*J4JX31 ³FU74ש@f;seq4N65h%9Uy6_ϰZ*b @"@zoؖ%xՅc$v-l]ES4AsڔBy(-:V3Jf|GduQh1؃ 7&I mFę+9DEIdMmrC [11q$MZF%RچcE3|\ekBjsgkKRdae$Yq йj{{Z0 f!Td25HBҹ,zWV^R#" " DhP*R(pc!x_x=%mAr:GD )M':Mb{:NtHXs|G}GauI^$73^) ѱpaj^{y*" YCSWsʖ^%m-!5D]WoR^bպabokɦ~< {)kIim6[핵E^YkIsvu4Ihv1Gh* yv}u=-bn]_ҕ S?/ _˼n~pChc ealX,.}۬~kvɶƱk[Ư[5e~u{ԭz1$k᱘xXy*" ߸qvt`l:vOHkuz$+sOaLp6R6-^l ܲv<[>KihQG]O"1[} y.]SN5VE1v@7IBcפ,VU#@o]w]5uߝ+.e=dEO?m{^~yR+ɖu9 _2΅ҥKmܸqAFƑENMv8pg[]oV]O=5!V_oz6ۣ w߽L .=zO>r5=%/مҔ&wD r[4vF Tnl}̘a]ru+#b;|7>|xVZZj֭k:t@ A#.Y 1YF>h]=z |QQ}O? ?+g{l}YYߋ~-cˬ!.<(!Co<gڵD% mp-m={gm+ "oK// d9Q $@E^n_ߧ̬SRyYPAZzx`=~7V^PdktH+&d8hР0?#&L>N9Cc== ko/ӧ l#Bg׃zkn߾}Ü?A#|?-H NǞE7({osnfW_}uhM]w)C,XĊPXRXS=C\zxkbkA:S'FEte ͟?N; }Ŋ_gD<%KȺ{/W`=9ǖ6gӟRx徎Wx枍,@ZKQG><GPǎՅeE1*bFA4<ާ/\JD܅{7biW#7Je &c/!VB?eg#7K ,(01n#$=" ߾r# \{mYHFK"N:zc)N&,&Ǟ8܀Dj"zU8`Zj"UelDg;c_}`9zVX%<bj4sn\("YLYm Ɂ}[o>w裭m=3tС{oEǭ@Z1 pb @&bsQG= q$h) 7a9XUKDn/ B܎b'܍c\:6U*,U:t{/VL -@" 1bDX@ĉ=~ Av&`- Eǻ6(%`xÒB q/^x-\AD]"klu`!JX.t q}&z:>9r~v Z*|Қ2]@Vӫ8Iav4,͈ OhJbѸ1'*b`%?!E԰8}5`QB0I3~.%`,4q0\X<"@?Y09%wJ pq̽]yRI":u"~lnw$Ai=eA*9IoTc)i˴u" " YC@5"" "GH[yj[rT"nmkX'0AZ1O hA%[gybh?8rO?][g/}Փ4i}E<`e/{o8&5(h`=L35D'L ˀi(ețHGH $mqQT!r`(mzmh MD E4Db}\䓰^YOD8; NO/et:Q2G=-L<9ȼ^TCh+\&M -1Iz׉MP](rW'$6vHG$wҥK 1k;K$ $ȸH@&ȥ SH 7x#w2D8č=)$Pg*]~ {7~e~9+<0G#D=%cI(2QClR2Q8|ɜnX+t$M@42 '@@ ##DXPS[mwygF#:{GÈ&]pČxA;č9@k6`uT8SyA-'WG O^" x Jn (Į KaNy7[o,;\/ ta/I,6v+aNMIĉHPhڴie9!0ѣܠ~`OSLe ąUW]evXx-Ұ?âbnaIxBO鞙t9Uu O`!`!`!`!`!`!`!`!`!`!`!`!`!`!`!`!`!`!`!`!`!`!`!`!`!`!`!`!`!`!`!`!`!`!/WЋ8Ϻ뜫+fa@: e׹fj0>tƏZSXla–|_W|ˊ C0F *(9_$5XZo:Qĝhe;WTRߨ8"4߷$5>NW^Q CѠMq=GXE+oDx\o z =\~?S;gtrv)CyWѾG4EsO%LdZi!SDeÓXVq{d!PesM'o$BHι 7;<{~{W5ތ0,$emXlu3U)~YFzs#Uޠ2UjT\}QP"D?_R^{"EϞ8V>%wA˦^){T]k Dw3*nP_"-1uC>4('E(Wي(~nKj-<.P^)>GpZ/ФGkLdHk[G*3)ܫzO Tn>_TA=6 C J٦5JѠxV JssyN%2Et≊,;?*Cvtȧ]e׵+V)STv)>R"EH| d$5ԚB#g?&2֨:wk=@ӝ;+\Y}p~ 3aZf 﨏!-{Hg0 u}. zN'S$o)>)DU 9ީl{AS>6cu' 6<\oKœUlCH[b!L EZFdEuh@|Gϭ`K<QV|Aq*BR+ F;_TDJY&C? Kt샀$( AZwf("^""2:纰4 ޭk!], \A]CU]{Iߑ@ !v/T')d΄iwҹGi }<}w} FwCX. -?O&!<%)v͊=쾨xjVr-!Bx tޖB+ط;*(9"҃gS$B ި')BV,L!Tiܿ($d@". ź|"9]8}HZ%/*T YckO4**GH7/LIvH[nd(RJ>Q>H\}owf'įIOډ6"ݺ8*`}BV>pu:+=dpB]y=KC*4^bR$ᵩG0XY|F})4N8K?Vd>oy}®ݰQ!=!ei@oݲaAؒ4hn/_Q$Q.ŖyUxg$*l`= aRZ/oHڅZxS E6R]+C+ݤyLaQ@!1rQVz~P|Hlq!=&VeJt:[[X}4,|*}!Bx[4^߱IqBV$bT1.ŸEGF֊o)yFC]c@b?y,[yV/PQBj 53#\,uCf2 B>뼣!%jOTH!=ԏW8 "!Q$Z95$Lԯ~x$;:x3l@7h|=I)PK"5}A }`oHH"{.2Nh[, !":# 76<"}v^Җ'PܯmđeʲV tQNlC!v5IYҳ>4 GE|=$WH'3xܫ |Lv%?B[X!v0ڧߐ)ǔ2hR kUf}'GQ :FC!KR:.!2b! ֘L `{;v?-X-VTdC>O*~48AQJ Hv}GV +yɇUӽ 8y&K5xw q_piP @"/ӏT~QHN'{:b6"9Jy@LQmR^WAWRKTwA3?ǁMnST}J !*Jxz ˗q`="cQ)Fã?u?ڽ! Q>4җIw$_S<_~}Q Ԝ ViH;"WzSU,Ŧsډx1K0 C bO$s}U\W}ť:6)}ParElG(~EwU~~~ԙSf[0 C0@$;W[[[аY_%O<@^Od.HnJaa!Lǫ٣@EتrFIz7(s(27Ԡڼ]^qH!&<0ixEnT|fu{O -G,ѐM[b x#&ො' ^1TN'qޛ5v7hP#W<+;;EEEQsnD{RI";/Ȥt"n,m^Z]t 4Am򫬬tK,qt$>PHr\ӥQ/ >hlzQLe0M,.Ȯ w+nrL0n擞tɑ4j iޥUv|]:,/ᝤNIvv!`!0j(w-!> &<̕iq]^III k xs1Ǹ/ tFzGEu BVU/ᕘ*;ڽ! 3gzp?=N/>QR^Ηm%J<ͮ'ֆ!jPeM6}K_r6Ku8/*xSnVZs(N7.ZH>CHɓMoȑiQr24MHD/r^tE>=C238Af7L{0Ι1yCE<௣iHnk=~:m19G>' /'HL쮽ZOݞ={|UW]'nHL*#_CY0B(vW=11>qABǽ I Fg#wڮ# DH䇺i !"񉺇ɏA %=;q"ͽwOP[2 ,f܁I-<߉ W>|@9Q/v}O>kh~xr0 U:e^w뭷zƒhonCLhB * x#77M4 Im?rF4*'|{03 8 7vk&aQu_L2DeY~Fg ~&~uLwH~HA"=&f#ѭҋ*HDhJ@L!j@9yh2<쳞1İʕ+},ܹsbŊ~fJt6aK @PS2 `cl.3w3FӞhNRX$JvDo'&j-i8@L' QTg # _}KҦ0 ptB eiѩ 3:H[: 3D5:2F碳2lFĝ5#MJb`z`qoK=m$y&XB&k4~BKrVFSIxthtoѤv+H}g#HESv! U H8̞=.ݱ')#Je!8SL81'LoAS“Ԣ:D΂!`Bi0)2QmYZxެ*^}!`@TH(k6N O 5*2LwEg!`q# 1$]MKSb{MʋR0 C  1'ct<؎C0 C r)B;D:>H ZD_֚ C0 "޻FNxt.ԥmNy@>=em!`8ԋ?mӢ󨉩K“ZNTwkgϽ1 C0 F@ `Iw)mɾK“ZE Lw(ѿ*(5)k!`tΓEv}oJ kb]X',Q&Nv!`!7:vA*AOH?n+Ie7l!`ĊBLj{=nEwlwAQ數[tzem57KD6_{EE=g+Q8hYзYR"_P?I ʔ7PbO#›(˵ dv;}%*"嘩 ce IDATN^u\6i> ڐ 4@>&kN?ov$5rw,ۄG&W^y%K/cwӉ=܈2ZZN~u뮒3F$uq|:%Ve{$M(̼q}: Pn("7bKqvv4*-?ģ OB[ZT\^̔gξ эy奥uT&RoTTwpA n{TPUoGf+ƻzI^!XYyqvQ K9r=7r njO~[ʚ}bmgof} iL}ف/|h=.{ذ7)]>qS]f!W3ՂܱcOד-\XSB}mYzF~~VQӻ"<:3?h.,K6ll;ZXo [>h"_K@TL] sz7$a.0VU_GO645t/6wKML!WR.o<ɯnԟޙł! N獊x=m4^D^4L&@}](.G C0Z@ό'/DxG u"*L>c(=j!`_IQI-5)Q}K;b^,3!`@7ȑ5VHRׅ"'jSfEsU!C0 Cg@È'nn mnAl7!`$n<[4ivIxa]) > *vLԅ`ak{1wcܽ7jKt C0RX#[g ;QF].N+يQIoQi D>Ç%{2748ms ;cGBMw}['WxqBҵD C0zBk/m]b}EKhw nܧ>岴Y÷訏̐YsSnj6mJeq۱|衄%f!c1vߜ(wIxѓU "a| W/+3/NJ=MsMno~`5ܡC]cY[32tnvJ-uM:W,jbkjZG Ag[F>Ө-ҦH C0i\Mn_B{dTKZ_~ W2)<׿!gl{+5\3t;uTѾy\I*3Ǖѣ=AU4"Ҫu< WѴit!ʗ/i <87H;l'wqeow' Əw& 4,aC0 a:R,kh`<#E啖F98tސkTRsqt3@C8HXcM_zfrI'6mvB !`F@@vfbH9B*MWag{jxj"CN=~1oXOWZrs\NRy|+"gݖow۷{uN;:I BLG\rqRN>ABS:}TY7lHv[!`!2Ez#SSbsDC21GE8[%9 [ԋ.!Hdlmn{[|^ Z{${vm Xұ`!$nה83pZɄ#PS"{$D\{Y8@^CM:D;$'p@MjmZ| L9@z5TĉMAK K !` Dp BtKXegW{e- *^|ѱK9w=.KL ^%7\ 꾗^r#7*MTS%H~CԩʭĆ!`ORJxC"83H@Xk';$N+RK"qi;pX-M(9F/ܞ,_=, Yi8QyֲI9"A<2H?!`@J I *p Bkm U_^KKpJj =XP5zH^`{=C0 Fx C0Fx% C0 !` E5K!`Y0 CjDlR+o6 /#Y^~ODj-Mj튍qx{e1G|nYg!C]v'9 !]EH>q IKĞY.0Ya 55qdsY. GހW2ml+{(oL&~F7]T !;M55 {dfA4s 0ӧrҩ$e,i=J9޵'{WIcTѣܰ3ٕU!B][D*#![*ـt!G|wH ♜|.g'%"ĸ o } $@“$V:ԙ,!?H^7oQW:̋ C 1D4ǓCZ֓+LM v97W&AK/"GpYE/ pHv}3ņbԻ8𴬧/降[ ;^Nk,yo՞7  аcGHQȒ"ïc;!t-fBL`Fx=ctuk׺ƽ{]S l f5+^x;`Fx=ct@8n[ Cpm9ևjSN 4֬1o>T*Fx^CV~@e~f8Kby_ ŧ*"?r @O `([@'wfżyn:WY.g̘C+b+8VpWW#=0KzR#zUNsI$l16\ȑ:ɗJҦ* +^|HI`qv\9ڕNsŧ*w{m!z_4z>Ex}}<;ݵӨ(}ʗ^rO=o7s]}RTG \.j[jZ7> d!R."wg/m۶u!7|T$;ɂ!*Yvgavɥ܉ӹ )[qǹ_QDFuO?m-HF zpWiӦoذaԬ7OǣsfeEcP vᬰ&N_Z!dl gyy&!yUj'w~"FSe ߽SY>Ӛ!HtVel[4<7"8ӟv/XFC x8NN?L~[rZ~׭}}]{wN, 7x?-^y}9nVO?O… d5A{}_?mܸ_{gO~]s5nw[s+b=ir}Q_WT-ސٻCIQݿۣv]Ft%O@ b_h'zw;Y R?=JL;G *Muf__u]?Ϻ}Hܓ ~_y|'e]U w HlwyGpVr5Dr~{xR$"M~/-Yĭ\ғ'O}K }]ݓ& a"E"%=q@%U-m'N& 5* 5_ L0+/х[%5qD"fQ>6>pLj #%x2H!vu}ٞ::u $.ԖľoH!5cF:dAgu[bPwC;0_DUFZ$ bk(z!/)Bt; !-cfW;ګ}H8,0%D*UA+I.MD[76qc L)}sx,`= g@|PWWpi\I6FY4ifOP>5O֫~SNx:.3!*M?@(jOԔh i;CB$>?.wfΜ饷Ȁb@YPe 'v=lcǎ$ [^p/i&btl@V6 ɩSڋ:M~*$CS&eE'FJ=@*w5~ a_}F)ϟ{r!stDf0bRbߜ?zy ;t#PG#tGmن ;w 6b -Z=%mls/}׿A.%Ӵy'vXNlsB-䷎R˗{(@`VC'OvGi ɡJEÆH:!3s#]aՓ14 ":is{y3HRvŚj<ٛ/W]-"å"^;@A12n%jp`=;JUr垜9a'T?R }fk8@dۅ^ +ysr<irE>G>p-\O1 On2ῑLɇYLpX a{` 9!>4577݋rKkm[ ֹCSR 2!vPkFԆ\'HbЈA`@dhwIQ>dў}^J@!m=z#ZTDXAuY8}jXBBrrF8vZpUTa[y'>,/}195fuJ}}1@ @]jy{2q0k![!鏔6 Gԝ@J /ԑP[xb/ű6$u8UHOlGwbP]10 ŷz16Aۈj*؍ך`VΞgy' }R<>88U&WlM,ѺJ?if 9юﲫLv^i*rM;6‹Z,s ZH.*dWe'J9 $)`9 K\ cC݉S__¢%{iԋZ>iXjfHvp:a3efZadugŊ ;F[ᩉ$}贐8, Ȏjt.fLjT{W!鱄;՘,c׿zniBƤd RPs5.XX{ -mxeWޮ%O9e~:W5z^eװgkoO)ӉyAӘQ.׺eMvo4LZG ,'*mM&5(skZk8k?^v>Bf+䠲cװesD IE OI; IDATOwNsm^;P<-liv%b.StO~vpk 6Og 8[w> 1^}]۾i۴hY\u:iw=ZDbx/ﯰ{Ҭy45 [U}VB2>+1ҝ!!Z&/s%hȩ˜V̆|p,Щj-)uE ;zC]&bԞ# ŕkB-@|‡[Lk q2ʗjFuplC#Pmi{#Ŋ9R=ł=*| x\Æ@@R{n oWAJi!>Ϟ&tށ:ƥTG6궊'w39j;r Z7BNFL-Ď^ٓ1 +uhZ{EN2ة,`ƖЊ W oZ #`;vd 1VPgYcH1,zlGx2dW$)oxkZZsH/Rƒk6j*OHG$OH>D[?}z QqTD?c}RJxA1 2=\M<ٝyn^bSwj;w/͛g>K!jnTLϟ0385H}_1Hs@g҅ w}y䑞_z%=؟& SN𵳄Y&*WT%;& LH펄ߟp]Gx{WjԄh0qdi!>3̷rXQG[4azɺUgӦM~dofYJh pEA–,[vǺ{߃wK.m%&^JDI2,'%wΡBr1c*WyT4lt!47RHq`{G>ͤK=R>u8@D CpWcM6[o #Bb`lOƗ|#qKLğӉLh ȂD'2J gHi A2$ N>[%իW{BR!:H$_Tpa0BC@>KșFMcwq-R?CicXKbK$Z ѾZ'L ƛa=x[+/#!@e Y>:J31`"Aj텠2[$'T`4@⋔5 QAHɵ%d4XҤ|isbI3`ON8 &@9_Y`Č( 'A|~~K@o7t6eɶ 0!1bdPO)i016͛7ϏYLpipIoKiAx)RґWe{@F:H~]@\ ib#=*HGl$L$=:#H#x0AdvUw{^wD}!3KU0R'H`ɮs7nGAb wg<ZR*F"`їikЧ{rcƚgy=nP93uYnjk\`ơh{pQ&GcS i $Ԓ ,63ӒA^fd$*0f7'Nd}|PUB|iRy̑+|ȓA) AA - jff<,H?=\ dNf'<~x_-(<᪫OOJP =j?uj1KcMg&HF?aK]tQ Q'. A}6?\˜#(?dG?>Sݹ/[0KC&BL}Qky&$031HvI4&6z_j5v.Ev)%<2NtCg!1bc9b=AO?233eO@`OU W[yB윴5} til`)_*eO: 84 D'f ?nv/$LtSxVm7TwX=dK&MOѮ!ؓ]ֶB=2Ky/m[7I/k=y/mJC~ɲyu7dd $ґwm۴/_}U_7<Sk|SNqz^7miWV^ի)%<*+':[ĈA" Hk"C13bwa:3\ uW{!DG;K93@;zUY3}{~9GW3[}td*#}1snq3MݞgH}f.”m2Xj_zT&-T Yaao6LZ{fS(tI)WeFoZ`l}'C0KZHPhh^gݢ2dC?ϭ?Htdnë4Um5;;u(3׮ʜ;!Id*z @lHl[Q>~Tx 3}'[뉵L; S^n$//=ᥲeF런1Ȃd6(C妯;#e6P rQIEջh :+ [{y_0KD쨂t˚tov|L=5^T:. ?|w=8Z|=ބSfKƂur} po'?պdg>@_A :;T쇊0Z0*MшC0 /jw *Tf5'S6}jD-vz{PWv&h4" ݼ\&ҷIx2q/b?a1s,҉s 4<8]s5~&!Kz룣z kRޠ އ5^^"P4B {]9DJobD̺\' nPAc?Bt𒋯|3&}3u.j7IuI d͆7!`@o@7Ԓ0 C n0 C  7R/.cӣ#aGt^ Gʋn*hlʀf!tp+g}'s[q=8'c Ҧ}#T@ΟNvUXw,aCrqTuB5f!1r:V8u4.+l@{xdNs M{GG~td2>lA7cG`pDLF q01>;0#,$}Lƒ~þM5Fx]cdwĈ3R"∄3I765z]A~nZӉ0v/7x~^& ƚZ'NR;``d wL7 >Ш{3>Td$E7&:Ȃ4j-$ &9?9E[҉7Dx99nınʩ9)YH-weuVɬ:EYk0v_nw!n$4(GVV3Ven;ws Yy>Skv WoSܐz;zxZa =7nu KД>2GpcO:JSs'-;ʵ;' CgfHęGuӦOj7ǭvwY}$>H HNN6D}Nm>"a6y{YKfIG"=M;ilW.iqCa+ژ)ZK #8 !z0 /!`FB};G FVlIu@Xmڨ2S_ȶjĀ@ =0 YH?Bu-of-$(Z:1 邀TFxR.Ҵxip ހ5ԄG^2v^\پpEE{g*fWYU={&NN<7d$u-C0 !R Vky~{W}]"WU]} У/+WSSmkjj&Ьk; BVCN5MZ7&P$ʲֵ<B9tEJ;؝BЖX3t`l^HN*C!`)E/[憀!`=^O!m!`#o!`Fx=c!RR en!SҖ!`@J0K)!`}yA# +HoBwǽ :+k?Cknw7Kq$,{|eG RRCDpFGΜ IDAT555Es $NH$zy2ոh_44 Aeeeޙ|[\CCCr cj:3Dx gyݻuP68@}}۳g0kAS7+E)EB] K?ns]Vf۱㽨;~ܙ֯_`d-(}M>LmڼɕJ^kfVtYbWW[nfʁܪ݆ ]%}'8H.RXq#P\T(kq֭N7&@'f]r9rXGuThERy6͛7[}$>׬YZѪ\aAlС%)J`2 }RzK@< Te{q !5d(ڃ'77IuV]U***\vvHf!>޽{@ċ҄8akniuY]W4VenQ.7'U.H3z8 ׽։TyIwCCF;v.\VvAdTsnq3<>:3z)b1֛oȖhm)y44knnv˗/syÝp,_bInMlڙ E%u[oƎƌ#o=A'voopbOL~^n>ԍϽrc4iR`%|QeR[qWpt'dk9c{gt~ԣ#]':4ɭ-[-\;Y1egg끻zʂYaǸ='|ĝvYn (N]MQ$߿߭\\SsZ՘9tܮ ؐ$v9~Qwʩg&K 7Vݫ+V,u=>8dg<UwCA93܂ݣ Vꊋ| yUUWm[7ŋHF* M O\K0KHP9@;nɲ5R=LFrs|'I#B^9V5Znz.8 >T48O܂3&X 3:j>ܑp ubI="/Ǧ][fƘ7dcR :в,9-:r9"PIGAu(lpܢ3{Wr.SCȫ0;dv?=4ΜLrNU!Ǥ':ej2!OkBFYѬ^>n[x?0 ~^r{aC0'Fx Cw*6 C"`?0 ~^r{aC0'Fx CwfqVpvl# o_R6Zov܆*HaRWRs,>X]S]vb<gC9茄ߍRB"'%mI dFzɫCzmEZ`&l+&dWնvQhը0yo4,6ЎmCD~DwcvkM\W皵=b;R-5ػ"<گx}MUծ]~B!`DUo=iĻhqt_s;f_Ӈ+E3 C&2] a߾"IJDEv1=4'Y7lryڹnn!`4e7+BfC$BL!!#^Dz~^GܞG/]DW 0 C#y9r:]t'᭒ ?Ћ*EF;W7ntp߽ h3\riiڎk~OA/t-:c|TSٽ-N+]P- {U!坤jkiht6|}pLWܐ*UaBA,DAwwQ(@Ț$9yM3s{f9Ug?9'f~^Ə[]_6' >yo"w?V\鏞~>w|)Keb-!L[ѻ%A/#ff͚8jYڳ^_U;.9~H8eћrr4SbpqJ,]v甸,8볳w%.Vnձ2܀5pYrq<9 z_h˦۾s9gO묳Σ?M_+c=}nԼ\pAo?plڴi] +Ua N\/q:nLeT -oyKZk}}}o>4r!C[uYE^D88餓Jè/?{gq衇4~P|,.\:衇ٳg'|r1#kb0@Lg!RNa(Ų0oDž\(_ה0滇r蹍gBFhjeM\m6-\W^pmo<>[aLj(01\{ugGkLhSk-BpMeqFco#APU $vo [#?S|=)8JpP'?)#ICK8Ի8\ҳR7Tz8|WQfUIx_r/~QFm%RS5\SSSZaRzaN Cc!m~VRxD];Sm Y)?0VzE$V5nQGUU*"0b,p٧>blw!#x[ZDJt$$dy 2U?{C#"c>7b≄$F oC) |,^T 1R&V1gk:dت1B5/}KG4B(WeMFԯiJ+$LTg,} 6Xty;1,>l_MmZ/-| ,xJɍ7X(NPv9s|3%-d4TH0Lwy2 e_E,nC.[p[neWL@a U?B _¶)?DFsOZ#A<BPB cx3\tg0gJۋy0X!F~n+(X(a-᳣> Q^xϒ\bPFaHb 8,YUXFSF Tm4?7! /%ȃkT0UW]UX@v*mQz>OIRɻ][mUv6j}edR(ox:`6zKW1m# -򔷍_塆/VZ8`~m2a0S[BB&q` 6|rk_ZQ[XZpH)٭'Y UPh/kC 3wڂ>=E+=Ɣ8KG! ,6)v=RzokM7e\™ ° ڇQQ~vg. dȦ&jNLs7ی8^bv(3e^ѐ3X -Ӱ /:a ԐSb?w\&&$I t2]a't%;%H#dY1Jkċn9\(SXj]v(@Yxֵusa~WFA{GyBʵ83n{MzzަJ+.KN 7v3 "iҳ>'?Y&đ2$vߨ%/)קy-^ ج mƩE_d;0!1SdM:ۣ1) /Tr;޶N)x݋0 CDx6N(=3c\cWxiW>u<3 됺7\kd'+ɡLLd2B%y[lQdwRd~\ZVS4cڄC3 ;)qH?)1`bM5b<[)dXg N$ܧCc8SJ%"٠ʢvEjPgWu+vJۼ !+,hBcq"i!D()}Gx O -TQvZiQs3,w6˥ `9ɉ; _Wlz؝O]WB0rqfwt.&kjsGݹ=?mUx2iN϶CuZxe|Qkhڦxv&͓HÕVNP SL64C9Z}s0c|7 g'YE~R묭3}=z^k}uIV8 mdddtڦ()Hb!JV O?q⒭dma) k, {#ܗX;s|˵mSx8g!ޜR #&'dS{R~dzXGD`[xJRuQvGΕF`mc-bNn[xaZYa~[k&)# |7JqIިqj/-~k_[ =yc8TeVb^9&]O? wl3Zl>+i;'uGfgv,oRvQxWiBʘzki0$x $iC86Z٧IDAT2 F<;PܳC'N}k[? G;NXA0o;Հ)/, 'tGh <8]qv<& w''2/9#d035hO!<|kpH%'h=|b ᓌG|mTڦ9)ɥpHZFQA8"r c:"PDTdzh.Tԍ.Ww#<u#`{a?^D~J2puwW Mji//}d.8|(OFƨ;bL 98vWk}-C܌˜.002EdDX8-͞4 '$f6BOC3<HC管U4,~ D_lPG<}838-o?uө|p擄xІ2Ek8Ӂ¼s|@>QxDѝtI%ۋ\cr+2)N|G"\_Opٯ;\hZ4ވA420xBJ )zT:jw(Ks|"! {G),ݨdX>#6Q>)m.&W'xX"H|z#B:U0lc|j#T_\z ~tdJ] ̌P[a%^ F'T<#ޙLm @ sQ$?$)>LhXM)1Y% 6MI# zR'b|rUZٙ5k?c]=ewx-؊L;0DWB 3\Q>,&¸u£H;xqfݸ ? /peոGS,KG\,ܴԡY0 !.ίmmxn/y%sI5w#(pbb>uRp@&o>j7$HٵE{oh ̄D 5̃]9dpD`ȋ hT ta7гg<Gki3fooCET'KQT" ݂9EPVdCLqDF;ڧ8:2Nx ]2r|:ydO~-LomSx`֋8(&A!ǒJֻNگG!Mɒ"K^z1|W]k_i:ȐII6bSwR$ &iϳ-A4_)àve]!EJayp lgy/y`!P( NI2-I"17x;[Jڴ8WXp!- IlRK, '-ڝ*SڐSEM&xd*sټ;4잹6KQ^dB188_nښL;p pg)m0qw; <0!7IhR_1C?#;;`K U7qבqd.)7rA"RfgvMm /YJ)33퐒$ 8=8,+BS|Osh,ߓUY{TEj#Ձ)ɩ.BY;O/"P~)>gXrnZGȋLad N^0c7o^9V 9'FG>R4Y)E E  ްzC`wK PrI+Nz=y9fak܌|tv!IDidɨLXz_#,ax` N١Q _ V }~xi=\j܍<34]pw[J(9pGh}r E+P0g37<0|ԃg $!3vԏ?ETtrvwюk,HSp6bFp{֧V7Jj3fjQ"$ U{|+~sh%  ]} N(PZ$` `5K! tf6߄[}!$C_]A֖RqIS2͘ <&rbJuIS.f|*+K (dWjVxܛa ͓&Uzѳ FP+h: VB +VB;!!!!6 ޭj[sCC «hѨ'C;C C C C C 0]5b爠dddd J#)Q}qMRY~&C C C C CUDG-J#)gm; ЙVErR.#oH_[$v9w.;8˹Nȥ{dpLdz[,sm>u-6(=+ZT~`q靓Qxg2B pLGH&:hrzxB.h~btiVyԕ<e~n@vq3&ys~O &? i~&9#):^G!!!!0k7ow\W]xᅕ .\*}hĖсC*ҟ4dH.݃+w !ݨV JC=6-q/mC fI< ]Xxq+b֖ =$ݨ7AK};w.nZ(Yѹ9ѹmL A,q|0]=K.ƤR|pFf>NhL8/gddLfXf*@,;e>LfgddddddddddL.p חeIENDB`amqp-1.8.0/docs/diagrams/redhat/topic_exchange.png0000644000004100000410000017106513321132064022167 0ustar www-datawww-dataPNG  IHDRCAtbKGD pHYs B(xtIME #h IDATx^}`\ŵVj5q{OQB^# H#@ @ByWcprmz*]{ڽ{7̙3":h4F@#h4F@#h4F@#h4F@#h4F@#|OīN b%27"L:@M$hw;")ؙ(ߡE6")F@# |*C${rl=|W2N^ȵx.>Xj@vܧ UsF` %ᡀ (aJ!6#nmxӀ'ڄ Czg$b{|pbY$Cg#[p=ID:x.;m+ 2!-&);k`9!Re~\o}MD2XoH~ @ɒŵ U][Q@<:7bMG<|lx A"-W5Fa$<#;Hm.Xx~+]% HY}Ģ|Dܯ3%DĝδJ1e- ߳FC: GwlIx$8V iu ˄Iemõϫ4%.{=! !߉Õm8b~~#<7 i*}8qg/'5݇,b qy!>}I#hA; [AGj aP*Dq=(Ng E$=$p>?H/CZ?ߜ. 熃;I Gt(>SR]r"ݻqr>J&Y8_gK9 y1צ1C}y  GǶo<~[B,nDU#1$||F@#8˄|: }!|ȯMe[~otvIo$hĻy'T8A]JlsmjJu*7<HsKAyCw6q2\$;JOfܓߠ5p@cSđ>?p ?Nxg}$Hn6EJ$O41MgBCo,̠KDF@#pZ9:PGjBlǀi(#^( RA2߃A߇!] B|~)Lg;6S,zt:A@$9oA|*>)-F܉<{|)?P#uI kIv~~JHjIsXgQmX%ME? oaQ$X| $y<Iuh4'pY3f6Gtbިj#9m'a}^zH=*'5<ވBoHSHi1F鍁)3@JxvI^g2r^睈Da\Ne)j!1 ~n?Qu5G4K|ƌ9/Y#h@7mܒ0M4VYiArԟ]6r3+R4f7 z5 ׈뺨C2@0sK>H}eIb`=)Ugӊ> /4J-Iy}߸MU:b ~Tr"iث ߏ۰X_nh } v}ҞIUuF@#Cgȋ41 ն` 26ہ%oSJB< w .zQ%{pIt8U4Tw' h-E#sZҊj@aFB ~,eZjR u*U:h4F@#h4F@#h4F@#h4F@#h4F@#0py5\Í<8Ҥ7;F@#{^xwܕerr@t$9Ң/1I^^^wy:~.H9ZGsȗ}} lhh GYo rG)cǎS<3ZW]]}]}xݥ=1rdСAgQtbԹ;rv={+++e߾}ra50w0n8~jr,u34d:ճ<ۡ}:bSz?WI>h_/NL233\8,ptp͑3 4HQ̙3e0%|qFdҥjilL.5)dة<(JXXܹn\\´'Y-jF#پ}(+HhNE!& j=#ɓ9P2SĔr9e}_Jn@i;ܗNmPkqrNl"]^(ƟhG3>Ikr#FP.՜A766wT2p2Uq:fM _`Teܹjzrѓԛx'!ݙJAUk$=ީu222ݵk\'IjIύ1Ez zٓ:BgGU|&"`7{믿VR -Zl֨Ɏ98 6Z4=cGKWqM0 KSIz999fpgφC=8gm`725myIrrz6rJJwyJ2g4:*55թjcǎo' ,طF)4:{"pd cI3P޽ۣWVV!Cm֊l9ɬi5jTDMG5%%%)5[*uW; o{!ZFO>i[l̘1ETi4XV@;hwҚ \oբ8qփ}R8:XsL'++K>#gy"/']6dKՙ1NLLT,`cݺur-sO@R puSNUP~~ڷ2e5 }'.89cқ͟8VZTP@3ESsbDB8qjĊK,Qp!q᧧ɓڐp;!򳾔z(hNCK'6  Ou서p s nϞ=53f)u(sp)0׫KC >fɚsKb_3,1egVpY5ʴ9 R 1+V|x?|:,ӛ0aB1PHNI퀤יڒﻫu>xbUh5 Xrj?l/ÇW}mpҤI(ArY{ ٲp ͚5KmKCvB,!gMp#s9 .JU;Ʋz†cǎn%6ā$Fi301gwWG:D7ߨ>ľ4( 'e℆푓VMl1#'$Lփ]p{{g`2U;8ְv5uWG%<†뮻T`曅ėh"չ8زb|_|Rչ dUW]^G}: `{UGadY̎HJ 90sf2pY'J]Xoy3S9sL|ɉ 4ؠِIX~G%.+1u~f>;$*;рHVx 4ޟ*U,Wwy8Ȱ\$/믫wwU7W 8"A9 /_\ϟA7ODe֏" .TmSXm۶82sgtjbWs$yP+>u׫~L;qe~Y ɛ$ !r2tEV|֘m^-㦛nR-f ,y*t#>)r#/qr7F9ޱO3K.U 1M Ą_{`5A^َ9x{8Nˈj|HF`6y){nwK .6XFgf CsB"$POUG#)8 9q5##IyQq0s$.ξ9C:Ugd=Mp6Ɔix1`NglZ Olר㳁rPb}p)~Aܠ kIbzrǙ#ŴHDt11bj 0:N/ug9Pb= gz{# qP ˶G$Ĩ3%%NxUMć1S M #G{!c֛ hlq0H!s1dsl#|X1mkވ%1$훑3eNN(5Xv^83+12(q4&l+c$YOWjiHċ8cI9HVq̼ܟmIUZn8 sA/c,CPw8Syg ~Q43+tΒوX.6 %;syYƜ*́ A 8٫ baLeOLbaB[L.X&l$'!^ ~K|8㦴a F8XqbŁ%F8 %GT9q07k2! 2S3F!Ƽwkl#<4ObXV JT۫6N;1bb /ğcE?#𽚱KC+Wރ1!&5Ēf{56sĞu4k L# 76bhX}'ϧkp ٙɘAdc&i_06 O@^xΘcriH'gP!lFٓ9U~Aɜ0HIu7 "$Hh zY#i+@$|ؑ6 ΁$j;; A)`m T0K/T 9 Ldx/;>3BlWX}4h ,|9٠ v2 n7;0O23 JNJElTr6CR l7_5bJ7#q$N|֬g^|0N؆5gĒ2#v c;ۚ;"6ll?l\F!I̗_~†D k`pK'n2y.x}Dt#5\ZQg~4FxX9x3KZQbqHJTwpbړ,;>c&<@8Hec :E~6FCwFIP=ڭ+й^g4Zب 欗1Ԇk̆8^XsM3Jex'f8%Ә7oP]T:}G2md]OA.=u*lSĄs=1 'M҆Q7<ăIL9R6+{ nWbA+>;&%s7ИC7N0HwO 3H^,.d0Tf6כ "I򆁍jnF[dh̟X헿qiDl8٘OPAI*^{zl CR/! *P)ѐ%Dpė7;$;z S]`T۱pq $i{O}Cw5֭XkgظYW(sgf3>&FTP,73<:0;5cJc 5quJc1Ǎ0ˀl.1i0ΰ$oo DLZwhN\g X#Yٍ/'ꜜsjnm)'#*lk̂fFb7>?CN":|ې(P@rz7HAj*dDcΐH"|4鼥!Fz~GNc6pt;=[gu40 wXRzsuz31W(]H4*Pڲ$=J'p{FDJ)  >tV>OrA#hNܲ-KT#3R\#3=8hh=RSwM@&t5er8wV%__Pf҅8@sKdmnߑOU_q4>O^3qvش ; 77>d--<ӯp={_X*OtZZ:-{[thu{W6JcϷUΖ'g׎Mwh]ۑFk3 Xמ +bj~l;ۮle8[Wkϵ6.K-輟>YӍfybBUS1lZi\ܸ:Y*atej.ctkk09pҟ"~+i!id,a9P+i̓3hT씨7f>HU*n_PVL/Ke6)9L&\-AQRM0 IDAT#a c%~̥Pܾ% N  YJDsqIqhI=6N i.Q ݂] b 7Jܨ ۱aU><&Ip}RZ텲FkZ:yvCLO ;&%K[ dkA\92JS"?(gׇ䣽X]Dq2?ϟ&hX,_*܌,!"Y]!o #Rn޽9|6Dd xPg%ǏuޔcT Rm3pʴ7ȘOJ{ w_ +)_,j6<}z +6(- ;.Q+-ͻޗR*C[?>dQΜrwP&-Ṋb/?I*Aĵw^< 0x`b(~S[1|Cn~y]o;͝Uo_rx rx?eKaBVnx& +Z}R_f q;?+yR̀9wR[zl~),}Zv} i+so1S;(R7B.6bI3,r wǯ~%Mbbc"&e6ɖZ;Chټ}ٖ):X::m/$Mޓ|E}"6v+~ʣˑ\xHV)m `NmQb<*e{))RxѪeLvgO` E'۲s܂t,*}JJ p(uMm%R&_8&gwH;18PR}s|U*ڜ+O:,Ս,yt9Tnu};`žyn]"; 癒 2G?]OQ ӓ(nE~Dڻ1)ـvyT|VL*0]R_#aAZО,+VQ9{fb $˳d?Iͱ,Jm&pJ_]~-)a~UuUȰ*6{{e3lȃ%BybU( L.nt/RMqݻ,g]6R^Q>1_~#ǿ͖O*9$Kzy{W]u09,Cs]mݶ7d-+;u:V&N ςT}VMbK&ޝ &jx`:E~>d=`羅r:)Qϯ/<['G kߩ&IqdǾǞj\S]OeχHf@8"='!U$cS){V(ͩN-<0l@gG"䘄WIA"%iu>"y%Q#&{իȺeO?cW(MX8.*lq`O(;>DwIxp0vӧ Uf=MZ-<ҴsfTC QrF;{ + .$ƎU[!y7GW?:V=\Y_(l (UϙzsI$7]׎rO .ɵc m|HI`TB;I$-E^f\<"FvaP&yEߙ"q~@~]#( t#%%<@Rr #r͘8rtD˳-ĄOH-D($WQϝ-;0)k9~%3id"R[vDI>0j,۪&fAQK$EJMu咔0^bG+ c/ 6Ñ@ks%!^lFHz,A-:T.C" c[b c|!KIv_.& 5 dz^*o.$)I߅w ⿷Cpn!/6"wMIH=T%8g˕d66%aB2+̅KFM|LwE|Fd %%q2)Bھw8 7"L"*W})(='W][,=ԢF].#û ;Qb-ꎠ (:>M!m% S SI`3wq_PD *>8R)z^KSR4Tq!KV=+5xv iRk.zES&Gֿ*E{*hh" =j 2Mo`>Z\ {,#>*pL ڰ\|;RkH-\Q-̘/k.{~ԑf܋Wnꄜl $:v-?-ƬJӤ5Jiٍ4/ BkVdU.08I:79{&2sbm8ܐ]l:K هVL!IS BK,Ȏa_.~3BEu(a H TrJ J\*P}o@xT :Hhl[U** F;h$L wK˜˔R9H))$m7j Pg^,1CWITZ;HDDto'B &~K k$. 3 RF3hJIx3_5YvD]k}fqj4Sġǐ@1d&'eQרIoQk>P76תITed.Փ/4]W_.xT|)m\4"aX*hhVjUKwԄPsw&5rgHX0kW@^ lP?*s+(u'Q?"jvJ2L\5W8@ďD|vK1TǪ*͒KRG̰ՠR3F8A=Kn/ [*DjSM O>?#hy1]JF:򬆿BQ CIDj렝޸ b!UthAIXRxh`"0y ¬iJRܜwȔ1ʚ-VTz>x7^`BbY,ZNLwMR:\ )PZ\Ȩ XEBׁNdքZl>TARO}gDM#Q7}8A`aQf}~vb=cP,P/VX˞Q3FNT[ۮԛcy DI&i4;ᴒ59{#PZd >2'$Ew@#!XM 8~zva[\oh%",70[&VPoQ䗜0Qcbh)K``Sk̺_ffc})I-? j%5`Z@OPMp U4P1ȏ׫HbKYbGɥvZ81=>ȍ/OnΘ#R@JLB=F6G@R#Ar݃*0PSjJB9^ 6E_JYwī*z^u+AZz%#C3~ [?dvΏ '/IISOɳ>DAJ \dhDjNb !卂Gi ur&f5PEJʳ0+1,#}y;Aɏҙcq&*<eJU*S$% PYPqǥcT%T4^@lԵ)Vf[g"ֆ͸PHbF!b=ZI!2":HB )ˬ!6kX{ T YCM 3!-ku~ y6ԓ$=AXsSѯ -;R͝%0cU#W6(\%7>6XF_W|:ԛ4^ِǪ`itQC6HÕ(803kX׺B$ #ՠjf CJT8eb`:u'2XRw-w$c矗='pTYd"vZ#Z\ O"(ZC%[Hvz`*Ӱ6㌊K'?z@wE3P*?iIT.3$(e|hs9hp>K+! ܹ6W '+M188!pDg('4։D]p(,9c]3GCY6`A|>ҩ1=a >f.9HkF\IRa4hٺcA·$+F^+\{^w%Ӓ4w,E]2DAikaʌzE?wQS'LL&;m $?_2JY}D.yc@iMyO^XC{2PuZ%3 X*kluT+7ZNijM.:Xa#*LuIj<j<rHL1 E8obyK%-jJ~yІ RdЩ,Z] j]._D # Vڠg?kO渵.q-7)F QugG wpw#r gDOIx8>TnY#)D?AiNcm/8P0svyr|bgQP-uv*!ᱜyV$(Z@^\ˠUY(%ƍUke9N>NY RUSb'k'1pKlNfcǩDag*##W)$0)af`NS}je X8΂$)궕8DX"2&6DI[ rg9 n+RdFq:_6NQR#HCU+% RfTp O`^-UR^ILl 8 , 0ZOۤ87P@u;-ág;qU=>4a=odt5yc;?Us-bo q& h:J4iTh39!D°QfV!֦=V+yolV7-렔2! IEO2N*xB SIC-j;h¶ 1Ho2h^TO1 1eC !Ӥ4ku̵{LfQ՘kMNi~Ac_ ;qɐ쟑kaÆ jO܎N};pp {_w$tăuvNWl\#J>cu]Mf<ua [px+ }я>菎әJ ̾HJFATH2O3`iH|}i9RKHx`t~6`LA8[ 7y"ؒL:ܪH \ X\-Z0/Ѯ=t, FEE*~c?r7ܦ P+ɩ\5JqATumI-xyؒh :x*Az_"tt{8i2Ww b81,Q{%ίVc`KլgA K b?d 1I ~aPe}=wA|4Jt}N_^Xt$f^EI3Rpx dͥa?n%|g z328{~K)M =ϧ4EѼ0y{y5V46dzcMm!^x&I|gwǽm!;^joQ[8FЄ6/K}]CnOvv6^\1ѮIvx3ȭ">rn譳qP^oZ]_-ӭ#qS_wz$#& ̒G\{0~3yߖ77O{Br8}q3nSWr9Q>v6߭9hSgI@Q }QEaJY<8GhEΣ5H7ϒθcҫkJpPzҔ_~imރ7:@uT'YU*l؀+:]n,dc*g 5偋NaQ-zk.`f"Q{%;3oޞWr夿Br9ZTːg )'<`'ޱ'ރӄfZ-7cJs{M}s,_{k̙~M#jGimmh8wt7Ejkj ߳OpOPlHkc3py u--!+3/-;` Ex\+aThknu ^6WVw2:JxF_O+WwWixy{FDDXIx"PecT^q'ùL?zކSc=KbYciOP&XRs}~"-IBסG CcQ,%<cfg{/CrSc^|kD^T{ y$8۲yn (zL *#ڜ g FOR@Z2SNlCQ10&O4!ICAA m(FVT9 {,gñc xyǨ8t.PC mpї ='=2l%jC"0YzlݖAr): \\p$Oڏҋu ]/Dܿ4d]~F#гٻl4)yPxqM;*`ؑ?9}aw^:AtuFH'R <shtW=4l5\ 嵈SQ{v/9YzXOhOv2[}F@#Ix8ڝaU95P+ ځ9@2M"3n=2v9.T_FKElr|7,Ly ^iU2Mh4ƍUrM:IVv4P!EO%ȴg(iz#4Kђ%GںE{Tt0R^IxNe<~ y٠42o=A=4~tɳLJ:jӡ`;ۖ@Pn?<oz[o$+$曥tz H 6L IQ%_8MEZ CqME6~|$ /R[+Hi:D :A(U' L|]~N##4k0+,u[Bf`Kxt/ =>Ըv+Μ36ˮ_>:ކ\}Ŏ7ox)^BSSwwsϕ䫮R6UK7H'As HJ1?p.7WҾ?_AL`.Nq'/$(%E-ċ.̧R 8ᵓ i טQD2j\&"ƈeOH-r$keܯ~Eb̑Б#U5KK̀<4;.F8ifi%<Ͽ#=*)9:x-ڡ) ln/9(QHQ}YnlU_P$Y$?;TkэD{*K@zaes$ +5y JyIJJ _4AY3$߀<줨HyFʥߩ=qzԽžRd!1p]%!CdtB6W{"6JxTco߮ԑݨ@R7[!C583#%UZHh=^kwNP}Fxarh[kI)JPۛtFxX3V+#PI ,jt "<RQ ȩ٦b$1#?/ުJVR0\2~FqJkh,HOF`!`?,)- TqaݦrŭS=sdvڛG5"%7@:*) ҢF)\jdF rd,aG ,YRY|R,۴I  NKSR Grk7bi': r4_|Us)xZBo&qq hGYT)hkg~ppRWMhm9eW_u>__ck(2Fde" R,+oYJw[Q?=wf8a4}lhŵٹr>Q+߶M犜@RSPEIIpJ0I3kC7nMA#`i<^/E5<[;6Tfu&RV\k*5\k )F?0.h[1-8BvΤЄ5^"pv2X1[ojV:`59R-C[1!mʧ o@Xb;:hm6W${_mȠVyxn&TqJ<ѢM;ye:"Zo=5{۝_DVX)t*S[@7wՃG`UqG +-ԩ7 -mab!/r67[Mb d؋\N|nWyo~#Ep\@v\CV3^F\{Mđ;SM:8ׂ[fAVTȐ՘%i"<5p&YSpD.pXn˾<>Vgm VL~ZWG* ':d]Mo0kxIC؞[[륥z[DZMC]yG21qxMjM]oaTD[Opjb#p̧&PjBi, 勴 jWN|wo±}| y ϱ]N|CIYÉ'-!x~~dQ*6_,Bu I6#+;r JTgT|;p5 [w Ϻ&WjiФ! jی#`*M48|4jêބ@< /+U"!AÄx8ka-z|t#"%" Pm)ﳪ5.OIw1cq4XzΌ %{) f;wlZU[n31[$*rڲalJOҋ;lO:DxTi֌zN51 :oxXS "wp"DZVZ.[q:u=ƽ'>㠧?ߕP &V^O-QgKhH/4T>8X,]Wʄ瘾mRP@2^+$"ldީT9ho/DE_zi߬bqN(f KJřd,~z'@o-ࢢ;>cviMs ^5ƙ2C23T9@vA' F4Iij0INJ4Ng@[ qljo_|wnh6쟒VA@B'.^І5vk[?Yx'vMEOGKd/O O&Pk/*G 6Bw8dP 0$WK ,.rH__f\^ѧdWS{@Yfޔ,G҄X=?ީmؠ_+ ϗ!IX׻G MYp/00BYb[Sykԝ$FoPp_\aU-bxop)+FWp 7H|}eHR|lTTmYOK$ ] Z+"BL&CZ)s*'39_/D ')$B=8s҂{Եx@/PK;12:G%='k=P"i)P:JA`~>w_]^*ۏ5H^MY.a ~x:ĉr%'Tbe_Jj$(ln 83i..ի%>#p؉|!Ukm!(kl4!`);"O,Bm^pok=~(_&AwS VARB1URoںC6;e ` mJ}I\_*Om3i%:zSkŴM|{{@c5  X|Njel.k!eoCOնr^o!F"ǷAZf ?ɩHrIgҖ-iouJnY>1?xu M `{Oh|aDo'vI"hijE>4էQ҉yMjڼmkJmswJYlRV9Pw^ 0?xc7uY7V={B*6 ҲRߐ:AEwjJ|̅2vM\+(}r}I6J32Jnɒ%O{}1߆( d:c#y:ڕgm">b`MpB~û7 X`qj{ cSĚf: >*9go?,z!LQS#ش$] jSϑu?И>Mk/DvIeX GH;C6fꈡ/ @7OC&.JJU5%0J GaH0$[bPq4.[|'ɴu2u\$Q~-ێj3\lAMwB2eÇ쾆Á^oZ^ps`TڼTNP@XlRb}V}Baq=$a!RҚ!:T|ߙ >P)rX|o( Lji8XȬTa?]Yz϶M6BIZX__N3^CmwmOȑ#;d( S7ߔ#5k,Y7*\p F*2;^_r_^ g_*]#ϼy,HB999{)b V|$ W_}%%8u`˓0 IDATҤIr ᳫ"3gcgnE~.)gGu)=z%RҰg` 7$Ra)0OKvah6:QY{O%(yT ;@&쪫R]wP2IXf~y'΃nv6`L8b$B67X X>g&߳$d7"IE˰ Laf3 FuxSي)8M֐Px4] A%,)j(!١*Y|Ve-2wy*+\ %Zۨ.$_ JSː1gAp>!LXc=*gY$%38Cށ7_|Qy?.p'1>cEDlw}^I|$Dc=&o|r;d ,A#z)پ}s=[o8矇κRb? hl饗$2ψ+Pzߖ.P93<(Nق܄vCnʭ?,.ÓJK? yx4V!T1S=4fz_j2܉H[Гj†x) 8& F&%5 HO]iSYCeLl/oyu[j[Gu^j}*)G&oMٮ A[X(`,D@u7$ 3&$( d/%}Q:č(qb uaR6N/٭gKC{l(ٍ$Ӓ.,PNG$,la9L.0ZhO2ՎܙNҥK0c 9v|sO+Inz>Ō~ǎ/H8 E888I#j"Rzw߾}JTk_ϗa\j<#/=pY[uX3H0$(bq4$ym-ſmm 5d&ATG*x;B|vfv"*:Eښx8 $:9&#q:3oI0!]ݸ`}&)4`> dHZڏԿ\Ug+5ՠ7&j^x{a>G姱n7TݎJ 0uEAF_o*.٤OI?|e"#}uJr0"}$pF'&2, +9HqR,{XrE>$)0sg>бj*q^W3<#1˲%33%qpjnR߶v6mqȉN1lYh1{-˲zO~osbevŝ ]Tea B@p\-]tT9HZlF]AlF;R`]`|JZv5vL4hR@.nUmF𹤷6,s(D$&3 1G%&VDT^KRI\ϱXb0;K.b3Nܧr.|rp/[6 ˷X3مZO EDݳ5P[iHַ &pPK\n.Bj#ii@oKu Lx2q1$Mp#肎f(*UˉKo-a7 ͇zB; D5~ܱQ֞tg,Y%oXUR %nܜϰ&8 N ':=q9t2sn*6!*rZKflMҌUEhLhz_jwa"`$$Ԝ]CO8#:A"n)eO8rr 9%|b ? >w)cq M %HFf-62Ak<$vhrПԚ*#lY7*+Pݮ>,N)TTⰱqZpEH~59WxjcX>Jk ؉ Vl*qs7Zo"֟7@ NZ~\GJI (H>J8%'`(/XtKtxvkNJq[ox#csH6źr"vR}ņy֫h`BBK$H5jz7u/J峒n⦔ʼnA;h) {LG:UH/m'W?P)EH Ӵ$cG{%EMY053yjl-UoDmx!YuN\ ox\p\Ջ.o[=п!]Wl:]beZVpsCN FNܤE2/7MIt^x%Mp00|K#6_eyCzOeMӊkTZ%IH'ebTGks1HK>RM@š!Hnn./Y#9""BqIe%"# &?񏱏h&uHv 9g?YDbXTOܗٹ]*J,O;&M\oCCA7؛obzr E*9]%QKWVIJ.H ;#Xp nnԩ1Z B}%& u6}.aGODh،βI1;2<5`[ <.lA"I2[Z:ZqS6!~ƠjUrE Ơ\Lp1PلMזA/zul (MtlYf&oq],QH1 jVcx;y/|~4EI;C`0w33r-e!I3C 5t}~.s#۹Q󏂰KVOSѤ5A L6),-NxڀG5+'gĪMhƳ8Mʵk7lgX\T򯀔#d_İ[Q.*֜diE'My-qr>9G*rnykƶ$R{ S(qpX~89'1V${*@d0>jt!\NłjZ1̒hBA- pa-m;G3iwۋסiCoӅjK[*pJlXݕ6D lѬ+=v/KzEG(@mYWMnwnbb@IΙ_+xRfҼamBU~1E2="L܏X( (%0 E\,+c|..HOo"2bQ!$6=f ҟwNFxQGE)- 텍$ef,OszTw)F7'hT+NO(&UYdt)U)uoR& -),,y2ĺ /i=z>)*C*ԭ EE^N'$)텯5Յ!f=+nhT$ &dg^2]D"L/_)ndT%aB|q4?+Irj,K&{/-@q.RYf)eLD)ؗx`2-8gc 4GZewqE cY{ޣXp/RNbnzunJ)TNL1^7?F5d(}1TlU=LItfOXFkʤo`?#V2wUi鉢_6䀜5xϿ8Z$^PZ0,3VIA7iƛ|_ .g/ Vلh ,ِbrq/uut#$LMi^+Rn]LRq7(7-mj3E@jawU"9fu9?!O*W ape;p+{㉛R,浐mUeNFUWXDR8nBĎ {]:H]"|U~={˺``KTt`|>.;A_gjN*Mf/`uLa3Y&y?Т{) Rj򊊋/I*اrjF:g$ 7 ߘ-IVI)^?^d؇]'tgB*,aox`HO ۭPIBXk.bڄ-E4@گ\_2i%&oҪ0_р[OA*˳(NT5$M\jh""&Bj@j~43=ШE$SDk76SڨSJ)>716'NaN2ܦ]ܦc 1 D y2L@N7L[֥2I_WD MQXGE]$9e\3>Š2pܭu5҈-";$` ޚ6U0te" /26Yx =I-D$'"Ta-Hhק4"'1sSbz6 ن&]rֿs&|utY"򦖝g}e;O713֤w d˃dTt|].HlRPIvlco<|ɼ X.:W^:Z^!E k D6'n:H-i@kfM#gY>e 8Fצ{E:CT԰2!q9#4k9}{AjS$e ͫZDZZ,go<}I<Bh +RdhjҷtkҗE{Sܙdv0}Vp1{tf=^s^TT!\! pTd[&}k@zi}}^'O4ei׷"=ƾߜ/]8v'c`U3-'13mm)WS'm\+XJg.xUd[dzh8zB+&M BMJ4 BI4@Fcd I gnu1:q;#L:2)MhjT7e15KW9{.}gֻ|T܎rw5q~g#r&9~ɋv(@.MڧN2_bt&h 9V R7 Hu _D&@=I@M:eʃ$gjFFmOр3ApBԙf|2dW.Fxlsxsq@ ;}h avof_1.3t\-bQVӆXk 19m8-c2@]Qp'U N3^Ë'NPc/yzр-EKfP] ?;k2p 8}F&~B~e4G81p"i$,g˩B/vHfNqX|҈R\c,C.<7)}tA,Ot&׀>ogx2~##0x٧q<xz4ؼ7qQ_7d@N1 pQ4Ţe94 hutq^#ޥUc<=-< $ngJt_H8qi%Hv{|j}kK ǷBK7ęd,=:qcJv<潅y;=J\.$ gM.L* oz"F t)Vo;Nc&@wfO8~sbNݙ&׀,Ӌ 5x#> ~Ohs oHQČLYTϫ@ 6r7!fBEEFEEa޼y8y$V+&&kyK=>Dעn--,\lGa/t: Ņ,ϭ[ZB5'd6|R4=|WgVy]b kD~R;E‚g _W e_֑577{>BYIfxvI.Fv3/Ȁz,K$QBD[BB¸յ,jjjzMh "kvQp8՘"lM2|MPИJ\(v1Wvقƫ{ӝV8uA"Uy896Div}% IDATw1e,YDѣG{n'u\\ q };}Uz3 7;g ^5F#2w12.awKXCO|9rY<:{n޼۷_2V󑞞iqK XKS5%'+p_}(c6իWcX9F$Vܹsc5 a ɱM}sk0xoFF=rYy-בMr(T?!| ثШ} A5Mazh ߲ĭ!1Ia^t) /)gvocJXbIO&uΙ32Q 3 xuԟ\zI>,rA5^x},b,t&B" \Ҫ] dVN$wrGwssq2PhCi>f&45/kn.9[lAG\G3\0ghd?Z 4DqzhF5ZNb<bWgpg[2ZH [#=-_m6T,zK-þxAam]Bf,L z-2qظf $GLtȔmp.a1C6}XUuXoy@}yMv!=ß)uVH0Ģ>vܩbhAH<ǝ"$R0WJ$.'J%.}e)`+"½-+jY}˽H\4*Xim= ZwNNaJ:}V۷?+z!W x7VV"þwoxTg0ްIVZN~ul͓ZNc{7Sq㺃t+.BلI>jrբ̐$"mh'W/ FNR:<@.}=)|ӷM7rAKcg h-ɒ(o15#Y7 CȢ y0FD]W 0@Վ֌xͷ,9\ X_{1όBtLҊ]? |!<ĕy> G"ٔa\X(*..Vo HXvV,1ٺ6dB0318e5pCv'N@{M& }zjŻq Խp0dui[8Az:.X%+t] nY<Y1z NGt=E6-Wtiorh`!~ =/*&.G*(D{x2kŢqE6 gNէlzG![R^,Lv^,laL.ʐG(?uh` 3Z"N6.,D)pX<ؗW\)}&lS5]w{` ;Uؔ{a!)=MK̫Q8Hȑ0\S^ZH?%L,B4pߦ@F仚8Ђ=v-(j3+B+xY=9@+>1=pՊ|x9(&>YQUvrÏ=tmJL7ٺ=ԝxsҝC@BTڀIӳZ`d;R-hm ؅e{/}Wϊ,N7mڈ݈N_ߵ~ciV$ow*Cz&>Y6`':𛬨M_wɂ^RA;/~ . V@ui`Y2.3]/ 4Ob`Dt W͙ 67o8˫ib_q>1LLꙺP.>5X -k,D)kn$Ah /8ate^HezC0{/CV9osܰ7^VIW۰~|U03uxv;o W8Cyx1&Y~1,3CDA> UyȝY8#뚷pDs?8qܑ 8DB[uQxХFp!Vq;%ѪMӀʮ.PIi@21s[wvTС3~"nkj^4 h4i`$4Hh]M4 4氫\MR+o"*YMjs[jG&g<ܗ>&CɱɌe8"*$Q\Ki?)3v4 bw'U8Gˈ~N*v/-,nfV2 $M8!:XorEd>陓CB>ˤT \1v2ᴓ۴0A us/wrVɦs{Q\.y*#O0V~*̝л|nE$YfW& ^Iش v]<.5uBBxPs3stHPXܖ5zٟ%p6"Ȫ>`Еk7u#C0اYx?|;<.ih B{{Nv އw/nuw9&u̚ Ӽ9h Y."'CZcyްjd`wmHn (2NW`3x^G) fXt.6X^mjϵL-NgO)I0̊K`wAVC;TF1#k37E\֓hYOUaD!9qjC*L+PW3'( pΌkQtZt7dŏB,Y 0p?7e󯼶r<r ;܂UhiΖ11k Z*aqFU>\vE Y#tB7>"ͭ0pO=>ͮ!gI.;8u29c[3zl; SMـH&Y8:pT;Q]ڐauՀ/-IFQ ''ח%afc~_[V̊¦WX&1&%:66zyZցk&ZI?>;/ vɊE!73MA+),F8?ygi|h}MZ tk,Y%V1o.tl ]ary_mnB? mO=_YňYwam `D&L):W}^CsL$aH/F;ޏ˧.*L,0G? LLYg_%"E e=C‚OÖ2גzV+Cd|.,: ܈aMQGҬ;`bQa 0!!-hK{kL0H`ه'PYTD"Dh"p+h lDAtHG "# <|tбxH2THDY x}8xeD1@a"̌JxJ{SVl&% O~ƻopxb H4n*u^/b2`+ª5\!AvSzvV[IPhOTn]A2BV|r9?9/_\[p<4+4 84{%?Z[9f[Wd@uIKGR'{ltI*%7qG `P1=%Ą;ahz9u~/9<%9 [nD=L]]S<׎CLad э:π{Ka tȌ`(cosQ4{LXk!=M,Zd'b3J6tTzܼ d]bgkUxZKƼv~d-yZ}M0:a[]ZP;fa^M(VnMl}q89mOpv;R4!Ja(jgw0'8m̏𢵽 5u-ʲk<>=VmXxޒW {V6#s*n\mcL,.hѢH;gA,ŕMfn8j\*VAOpA ,hjZnNJ DBAl $\>74xttvG18-a (ZxlHEcEGz~ t%Dxÿ`>xfl}kbo@2̐kUhЕ?tsAV $ (: PM.S.mߺf3ǎDu_"t96]tfZ#e:fd$Ig8I`VK+%( F\:_b+^Weo:SnTVvh"L+r9er_Y` *r$X CJĝgϤ+$:|c tB,@SqLVXrLצ;R9(@oBw8uYUy=,_iR`TDֲjN譨o*ib۾G`V 9Z뙉8h;s)Guk$SZcYO5RbgPƆqx4vc<DώH!;OMhtpdZ't~1{q;M/pfH`[Ѓ2 pzqYJ39]o=;`Lqokx$cx F: z'TTلt)kcr4r%IK4K/I$0٢qjSLhF "n\5c~)v=[ |Ύ&Z-<\MhVMɃ͌Q|?:u9sʑAOQϘRKh v-x))"-pk ᮄhl羒vxaB7aw,ȞN=-r!qqK:f:Z|2Ok0(!_KJ9#kf,3-up(R'LpunU_!ٓdN.0B 4C;?iUskSY}xȴ (0Z߾ Ti6䮡5cZ8{'R㘅ɄD@WkǙGk=$qsȹ1tB-OD"ə|ʓMM&N{Ydtۘ\ (jOO% [P^D{zb!r~zm3F` KZ~ol/{KuǝےR3"cqgfz3&WwS>8sTw3+U]xp>Fw' FRA/.U E]Bu,"5!v͟D7L SWo:fr K;q2̪VSW@,5s&)65SoUBZ;^wYa65τrlÓz0ToCqf>9Y .7o&@24hU8yM$SնUlHR,uFƳ*Np #t xbLp0^b4W"O.t0G5ޠF=HP1 dc=)), DYKr,i@Z=p33 vS+K"s~Z %6S$r,bhDV ^\iEn?"D@Nb_^RCnal=tN10$& ͗ a6՘-&֔RL-l(b:bu {rNYȹQED(2u.#O5JUSk,y`Nև+Xu "rti׻PE\gʒϽ(?). ,{iɒ R^΄qŔzנ'Ue>E'1;N,A5O(YW#㲟{8QypFsݒN.yx0rpʇ;vM4/FDZz9OόP?''-s =6,|7: ΗǵMbnJ=ɇ{ɠobӜkƵvRlć}#[*ʁ)ܙ$?]Ȍelv q:Z܆+#Ȥ]>!ܚb7"UT?$=Un @Lz$pSMw v1Cc(%P8=w#gOݙGkKDMwt3qɒ=-$Œh~&YPKx#v}V̯Nc6hܲe۾:&̜ >jk.4T;s0u"ԕ1͂h!KMWA[%=xUlY'V5%qײ.,:k4Z`D`ۘlXNTp!EZ諣EL 3?S9)3eʤVַjo; mĜIaZ *or&C3<;iwY#scN8όCEHxmIR?~w"e##$TŒStG㪜$RY:arbz7?O1PAC)?/@'fwH `1ov.T-n܈YK6煞̹p!xѓ'#2!~bt2װuqi+Czdb_Gj|D-=w1_28N;Lb}o,>V[ԝ:q 2.G4Pt#Eμf>3mŵ yo}?ڤ<>Tlfvx9f_q#ڛHe & ߇aŘ7fLblłbn<2r$_Kf\?W<7qb e-_H(v~[v?[V+νCemJ3k}1++}5'6a߱pQ_\^z-`U<&9{O.(Ȍ.A?t?>m #915}Rw5Q* ղLdajR4>Lw/+'ˇŚrfv1IQVDUX4gŢÍ|*ͫ&֙<< -i硨nX~z ~pu} ܖ[S'^O7u_~a7_=xr׋$ː v3)e_M'^?ZD.PfV&caV<SƅdGpP z^j#3=,ڔqI6_-b>?1LdBLWl,WF_*^~Yw ;eXhi55qenWr/NŔFԖ@s]1 YPUYQ[4)lID7 X2m]8Kx_}o`抻2 .9zG*EwR:5=5sYנ 6>=wPrN!H_ ф㲩)ɓQVqYVNw30){ ,Z{khMd·o@,Ymii(|+>^£| ͬc_ JogdRMöc-̖??plPPu/Y(a#5y oJva{O>O^LJ3/ok̛u3Ͻa:^k*ΖR؇Rg=dzfSKs0a-XfG$_9Zu5Ƀ';K@ڵX""޵aݕm쨒5+}bB$%Zfܿp"rRXb~),62f׊&I;X OUݓ–ԛnBc8__XHv?w$KS\N1CzO;B޽ʢ+1ӟl*O?@$()mHt2q/A;3'%LRMfI*4 ]d\u [:ƺplG./+y2f++p:>.DIsסv5$^@k"`p`3)ANҖ ە㊛Aml3IJ.1'k917cGљQ(-\$TK;k;A3KDGTMV< V3Rg褬b)rW؋\`ҿ] D3 bQ pt߲Ȝnwb +PO0C]^9981Б2Lu%==<,9 % /Kd$*n^#i,<;PNz I9*.bL5_EQ.~Vjַ0_Umyy᪩ AcCݰu2㜘>4}peZ=U>2/{X''X}ŠJT/{-ʎA+0>8r&OLi̽^7۪ ^1ILf¤9j#LJa_FK[ >v(]Q~N"g ]X3z l3{uHga~ 뛚˕edOnqk\[gZ+cx9_߀ -8j\Esfdth6k:ܕ*~X]Jb::RF? ٿ!jۥKU*RM14qD('A0jDoG$es9gHbg~72S q1#]W$L2FŠF- H`X7)j p<oOH liboi3v 1*Vex'NֲA1 #J[+Nx2-t7э)$jjkغU+)E2i5ߏkAcu)fJdCUdY' .YAWiSO)BiI$nCkP|}|;`3,4vF3ZA CsgtW&5ӝZɨ-܊D2f!o8h:\5'#?iӗ}5%PSv q~/ŇoQQۍDx/dLNOn4cwJeUGXD/u+[r+;꺫MY#';&F6ҒհkXt<.[6%PV@;]ES>qC e"I4`"`)+oki I׆’xqtuvH"نEż<?y6~(i͌m8^R-ltYeZIi"E-MME%T"5zC$|a$Oms2uߡ2`CWMob3uGq hdvlE+U 'KVޫD1=]Yz\HeÈgd7? B28pM‡FhdrXpb9+*FnC;%d҃"~"3flx,&d1!ك8[#*w׿FUF{Ayt~lBi-E\B2ݕ]n&D3D]Ab5Tg2IX؟Q=Kӑ5z,O8,FQf}$fT\$~6po m^ldƢ>Idi8"ҞŒfj4rfn44MQ%!a˾rDx!EMJP{WE*zu#m}p LA&=̹]7X!b}3 =Z/$U-cqm.$VvfXSUK?d]ؾ)܈Khy;'1Iń,s'`MfXZ&<onZk>,%+EhbT&¬=MM@roZW@J6fѬD56j}B>Tf.Ͱ3)ƀ6MB6D }V1xKI#*(YQo=V?LmP@ N,5I.2VW Nh$t?G"@T1+},=ZU&MBGQru p~inP ?YJ2`5mFo?𚮴2z26 T1ީCdH6ݥrϣtYo-҃#Ņ^m Uq|o%6B'P*]ǕH5rGŎߢ֞ŊMP{+(sM" !κLZB'62mح\HXiɢ BZ}mP)E{pR3[W~V+`܎Ϡ5bP$+4h^W7fd1(cʽ,ǖv@u_z9s&8᡼ =:fxn 1=ẉ՝Ϯ ~& D*r Vm4$Ws5 kbf/ockfՏ!%7''LͲ ر#-C.LMų9e|q൯i@8Ѐ}Wqm+Jj;h4i@Ӏi@M;JӀMƘ4cL]M4 h4޴4 h4i`i_I+qv\l>S|GXҳq oXRP{MJE3Y9qx[7 q59R}YWtۛ]lnʉhIA}^il LO&jJ}|wu~^b|ˆMj4[C-%Ɏig 鬫F0_ ir$̜'Ed'y>NnB Hl/E ߌ$a:q H 4#13M^~=EtZ4__ݻ6.9DƧ&Ӏ$il~z=TvM4 h76v4 h4pKbc'_?{Y l}Z.zڱ4 h4i7  zEe޸dEgg\{L@O4 h402kljų/n@kk;nn9Ƀؼu?u|@Ss\N7&f߅SLS{<`[$c ?w=8q̬CG ILIC[X-X{4kpj\8!3~ZHKKbtXNJv1M4 i Y OTY5jrjoP\R=i8.TUŗEMm#p> xo/^G/}'KQ]n6k^وyŨk»B-[W߄ 5c Uk IDATi7i@ӀM^g]HLS@KEfx=>vM9$K4kOo^vqII@U܊ Y)Huq7#3SvM4 F EGGq/ H XձUjrA?@6vC&t YI/n)xM D ^e!JeK 5%CF܈?yW;ܙqji@Ӏ!;9#UTuLNG.reĪa|[ZFz]>̟;Ue:cn6+-<ƞh`hj\"bӦf3QYR\RZV.SGi5q`Q,^8C&4 h4040dG [fL0ֿ.v,Z07]6d+*.GnNb蚼xg.q*jE%'e\4pryLJǪ+")EC5kFJj=)[d6NVSULl8m5 h4i@i`ON.3ie?e0kS\Hp/U5Ų[`r7LFUKG{Abd麔oq%::0bb"kSAx|[ Mmk4i`j`HOtj!({ >sD5I6 K2 ITTںKD؅E@O"DKYDӀMƧDfW6M4 h4io<$-_39]p|D%( (AQ1_} D "$A  r9a[gfwgv;S J]]my; C0 T30 C( L0[' C0L !`&b!`& C0xe1IC0 xF!`e f!` <C0 @^Y u0 Cр!`@Y `,:i!`h0 C,0Wl4 C0g4PDz(q ?+ (~x,mHJ} @  R>y+JJ @أ xxxuQ" J H$"_B/I4PE͘ptwH =X@PȰL: ѸtT&kqڊ zb\_%QJ(hoLxu2X%N6A҆,Bv^V._"N9EMMW2֞P:Cl5͗W$ n +{+a7i;%.m8jnGVPfyeэeƅJd*wu%T1C'JkW<\i80> Y`ZiiwYVȬv* jzl̗G 1ؠb٥S }wq3Ԃf=p4z4 Aw,*]+;)~Z_U3juO{e=di2ʩ "^zT]+X&C Q`@О)o0 @}y:\ۊ7[&oaWf4N).4UWJ}D}TN>tȦS]=NU>^~&/ɗ_S7\TU}3x)5θXS,k14W7]Ȃ K*TV.c{$pWk^+]H]XB¿=,)ҋs(7lH4ZL-,H[K52@A2kqӦKceӺ=p5/ JŌ:i*TUҠ0Sn&_:,y}2o[YfrZeW9y8nsebD:%ɻ˻gv^鉖2ևT0npT*Ă\W[=*^P߀CT0iT5Bn[jbT5Æ76I N(bKZݳ>ҡŁҨ0bՆe.Nv␫S=٠4YXT_kx.Kv?*NtԤ@e/ioZ#h6%d3' {.vDaX%LA9t^y/P,ݷt矨UzI(WtKeUFMjrWW k%,R@޵@nzHBTvLlHH.Y0'Mm'}Igҵxix]˖K+ ep ܂xs*樠]\m,^O벒  вiugaҡPaC;{uxeZxccۉ)xHEXS=Vҭ }pTd]J ZGnU`/Jfkd"WȆGʺ5ɤSx@$EL3{aYz4:KoQ7[!U>QF}mZ>E WIU6^}0ȣx(\ %#)kzmIz.(RCڛ1g ؂xzuxEדz~XG`@FIsb0 "Nk]evJ5:{ӊ!`!PL`-tT$OzWӁ5 CeXz\eW_KBhza6Z+x.Ma79Fc1Av^-wxu]VLZ0 C z'\d L֕A/?丏V!`c)AgoYk,~.F^Zq-"W1,_c99y{xJ*sfN`,J`WD`ցruʤkDG6zu^酵7]sVe:{.k͍S5.~`mMK۴]Y7D*VVE[;ux?HU'*,y**m4w}{ ٗ7nӶK~m6l.6iB V1߽ 0ݳgUDdSkG |S% 9Ϙ?/ElAH!_EHU*XcL+FRܦwTҟMYGuCﺍ^Op{ ̋=bLAhh@"}d_&%qcO"&$M#i(6k>kE$i͛﬽ob 51Ĝo=Ew:hUUR"95vcEک ʺuoU0UXW^(/PBנ 5wh_})7O:/ʬy궻e:fs )bV2GMէQS/ˤ"vkTOVrLs.Ցv@ZK .S Fn0Fx0 C+0E:Fl@9ml #GQ?餓V zMk˹(%ym'[ hX0^:c'?'[_|@zeW>?PGYdOl7ujX,^]N:i?6['nr5oVӿ>U ؋q';3L[)Pl6?HKF "> B=b 6nD$RQѸlƌYzX{+K]zH,~ۍH/[<}ik>V4.]<|KwC\dZ>ﴠĎ.2lKN ( \ʵ*!RYYYWM(=2Ye!ll 5ۍ7MS类C!mUeO Um=n\Vc lpFB55FHUM24S MUG IOOOh4De{f# n4nVUUmUSSRR2>!?XeXA%``|OU]+p;*.s06>@iw5*;;;oD"WwʤE:+_߮`aa* Cfe=>y!5 [55~Ry*&O҆ @V\)C^RneUwtkkI 577Kcc#`;Roĵi0gLvG;%c֋FeM 9w֊Ρ"Uᛠڹttt#y"JYKZ/(C/|'jZ@!-VtJt> J7%\+LiwTk}]Z%z}bBJ.YnexSu),J{{|wC7d6lnA/_.MM^Ȳ,05k{r(DgV"6Ϥ^ic$0r,#XzFp $ڬ*+Ti{LU8&;LMؒ?ӼJBsd8q:b7.jPP)?LsSN&gm륗^;Lf̘<3h"- Ƀ>(?X&cKKlv,xV \C93~ۣb^ӧ:U*4vwI*ԺCE8A{Vg F\ 2%t] X#/駟o9sfU~ڤ O).{GZoVfmZTHv6_%↼;dH[#x!+ʔ^| -1GTؤz%`ժUrw˧? XlYuב(}QZrQXZS T]U+NeaLsHEʡr֬Y2~xS1>:K D6w\9e]w3 kzk|)i߾Ow^ oO>)/[ z^F3#ͧD:G1E:WBCL`iQ C{=L/_*w(L` ؏8!`e#|&iW1vC`WG]xPRpMF~6D> }^Q}?77raAT~FFzKRXYpH{@;30#<#f'&L d h=w侃iZ!pyp7;0V~d'; g0Jtb]Å m5#qTr|L$4T}!y&M5v!x̄1CW'1< 7aњ܏GC:sR@ @0'xsϦ*qƢF8+xGI\/,}B]z`Yap8`p";߅vw"m׋<;}v^Bbwo71;΋C[q" h {sAŚ> z$p+@qGE(-70^}U`K nRe !њ404iҀeglj26>3^RT$0x1$!?G?rJ^݁%yPhZZ1X1䧞zɾgЃ>&뱥R￿ 3z.Զ#a‹f6q (Bm)? k=Sl ,$~Zin2j+{塇 Z-f E0(?yL f3X ' IX3 aȻ{}zKrp $qgpZ(e#A5_&OY8fa}{=K8_1c{#_g pn4ovҽWzi>z#0Y@alA!7\_tXx(i̅E+F8蠃{/>3zHK{)D'{f6uW"T b&TL|!VC0 WszB  l!F֞\ggvB>Lw4s$0`;ÞX A)b!+殾jFgG8Kս'jAQ˗+3+ ,sT2 H`|\s̀2)5̚^uQ+AQviXXœ W8k02pg6VڨQY$10BXD U$GGFn.uar/EI[cg~{;]믋>t.֥5r}>#X«6ҸYXեy"M:xċ6VJLÄeW\JT> B1+Jv|/&fnIDAT7?|Svw%fH&{² KE=X(I(vD\abirHoDw&/p)ǸY#5"V$؃c Yc=i޼yd`wrBš BD 3oE~&khW0,?t W&Z,y^>+Lo*',킃1@0TJ)ŗBAHTG ƅ*.r2\\oWgLY'eO{ǣr QPpO:Vh|+uV& ~x,8n FٿSԧ>}pL >M}h~DaDl[y &m_}{mBSզBR|ꩧO~/P"pΊKQ%⎻ :g͎IEtoD/ a),'R7q2 Y/g*(|yZ)ѮqӔϻ:Im[pLS>vL qsmpU˥$Ĺҝ(ՌA5we %&ӂݝo7bM_a}*B`#Tw.H\ɚmIF^ "+Taos=7yBB5f{0hF3&(z}Od#-<wk.X%Qb#Hpr49!v:]G)70^DrY /RAq<;'hy PT1"رTeZYdߢr?##ɩ)lh+Wh4i!`xD2 C(ZLd@*H]a ŋ%%P$|DZZ 8.}x,JX͒!%E(*'MWsΑ_ɉ/Gd]wUD--p~ܷ-袋Q!;<+#g/+{_y'䮺*N=Ia׾5s&*9Z@_la/d! +;J b=tB.Շ?a/.6xѶًC84& >g [~y"d8c \l ,e"1<kdO}% , h݄qgxV ˑ+ps/(D$Q/c> $`)63s+Hn.ao朮B_I@QA`F~6GD(H }}z(`K&k֛K"&K缍Xx".1!4ZDs94;RI^GtϞ|3&|qxT8ϐO2(Apq7NFs!ܜfRr"a|O4Z \_WO܆sRKFu"]v,{~W2 ]w]jLN*B A60`3>LFI9GJ&^AjPo|2d\>OzB ľ2 aJ#q9{ISc JmӸ#o89 N'+>[9b yG/b/_%`0.|F2 eDX:(g++21~Q^`'g.)%b*#5<žFx74mgRA(o%0/Gʘ[Dq[0f\x?y.7lwKJ[x9'*t X@46h_.L%@ Mh4>\hh0&J ѐ!xL Ɋ@R-0U0ẽs'q a€8!4KKש;w+Q^8|5/4i ?P*X`9K @vQ$P7!$I9J [ waȌ+FD=ԋ)Ղ%(uhpd\o]@wp{ wQ/1 :ς-V"116WCQq! z?a|G~\@mFk f ̀9~bQ>&c!װ t! `0a Sw1$8Ri`2hX02Y,P8lBeq W€!`*<=_>r".Ju`Б+}>\c^zw3";fUX\{.lp9j8+]b q#ǓՆ2p6~g̘(:7%?_Dls}htl2qY & #Yl p\0s01k<`ָ-&`:"(Y~AAh xZddO6w. >Wܺ s+ܔX.$o,9sxZ1.KjcfKZ1Q¥ɳTc`P<A!)L?`bW  I7Nwgz(`w0mX^a `8gobu\RV/P2$aSW /żZF@(0o9#^#Τ @G>ks:汔 }RpbicUP*& oz㪁Ì^!pD!=L͹9YfeChv'ĎE0#H#,53a .Kf󥘱la}wR8ׯ^}EAI{qr?La}3f<q]ôa.QXhV7k&J_0ŽsRE9whNB=q?.蘠 \n`X+1,q0#qQ+'"(kBgj -XHMPCؓ4[=9rYg-gc{JQȚ*#?` o>hyƋ`.絞$k HŔ΍ѝo!_. If/#H!UGUJl [Pt3,uoᵩ{I+:o%val&.aO;` dzϦˏ#JlY6 K6sHGV@`gZ`;9vJ"&t jJ5\Q>J2ס૴>Pq ag@HYMWRwYق %R?/+\Xɞ^UHtL8e_<<=w*{YT I0ro 0Pir P=R۴Tf6SWajϽjwj0 C0 ]\e_%Sٮ]L[+!`@1 ٺmʪU)eVEҩ5 C0rfaU t_RmH+~Rnzk!`]fm5ƾNph}5g}3ytZ K !`!SkT^5ݗN{j-TJzxn~Ξ6 CscV*.gn%ϳQ੕WL_ ;i%a=*3/C0 @q$uc9z*:2E.m;Nܹ| jU3! C0@`-$=Rj\oٍ*gRЫ{赧6f6f>mcyC0 1@LeJR c9C0 C0 C0 >j{zgIENDB`amqp-1.8.0/docs/diagrams/002_blabbr_example_routing.png0000644000004100000410000045341613321132064023032 0ustar www-datawww-dataPNG  IHDR3=iCCPICC ProfilexXwT˳.iy K9gA% * $(DP HPD ""("*ow&TUW[3%<<A@Hht~g> 6p@kmm}Яq_u>d {zGy>^ `H\t80f_Ɨap/=V3Bf zzx00X˟  3{Q ýhx؞臅'DG#g%%''#+kM{X#UM(K?2A%KX􊉌K 3` P:6V87 ap A1.h '`L0*X_.AX!v!9H҂ !sr< ?(Cǡ (**&tC3hZ>C;$`Bp#i BaGD!",DqъFw7Cp!D5mZ" 7mm m+:j:!:]:7D&a5zjz2>=(} m)M",CC&5 odFCFo3.D>ыxxĄaf2a d`4ĴȬ\|yBf1a f92ɲͪzuuM͇-mmݐ==,CÆ#C5N&NuN/tF\.1.[C\6yùs^a dzK }̧WoEG!UH)8_<uA^A ÂυT zd'I-[a6aDZ"mJQ 21Xذ8B\I<@L|T-**Q)1%ԕb2J%AZPE:GWLeYFYSvrbr^r%rO FIm|.(L+-O*(PRVTSZQTP.URaRVTSE&Q֨Q]R=H[ a  9->-r9m6ER{AG@[JgYWT7P=H-}5#]Hct!CFCb×FF~FFƊƇ7emeRcnlzάl\<ҼaajqⅥe-+`ebujZ:ºccmSbVmW{=3"1=t5[NNNsę9Ry@%WE4Ƀ>vp vNNqo@{8y\NTR6=M=};"}~%W,-,z-z}›e嚷ro;ni5|uw-="?|X߿) / _z67_~ ;}ʷݸE?D4b/do/I@g/ 8@G x o0vUD %Ƣ?aVSTq[x4LkFM_0EabNdg]fpr y|HYcaZ:Qx&^b^r\tel# :JbD*Kj5lt$tyX Q?5362hqDz5kv7n:695ovnvi:z`[{Gs/{4,A!ڡ&a>qg;cc?S'%*1}g{;W6{Yr2 [XUk׳om9mzv^6]=N.n{T7xje<87j>f7eum}s ek>|b7ogW޿Qj]0{_7˿rVn3? {qLQpߋ)W/TO`A'A$"&**&,N2˲r} UJǕ#T) hjlk.jhw4^+/172:i/$RJZFԖl'h@rux=}ce'łBCUôÍ#l"ݣ£crb+nw% 'Z=}KN,ԞeOǤS2,2Ude8>{!EgӅES' _8T>Z1Y9 g+;ըk}4pj0ghHhXx‰ɌG=1ͼӂb딥7W;̽zAܺ'FF_7o)m~!}gAI_ɯkpp8 y `kw B(@ a.ā2\w`l@8APUC%!0k"[C#!ȷ(ARzk ӄz`;ب⩞SkSWuQ{wQAp ]*=$#7c QĴ̜BRΪ:̎cYe̵]c[@BZHlDp{& mIT,+Vl`Es%9e*:ISZK[N'P^~XlIiYyŴ5YUؗS\,dq3ry{f-GFVGbb_' G}!:YS~fg_.辮\ƭĬ.}pY۰vyq{?w?pD!t qZs4/$i@ǘbJ02Qqjf>Lӈ`X#Қ>ӣף`0HaH97~;Z'Bd3JQU1بxDVtLg~My7_=`:__m#gooro%x@W_zrɈ}5$lAB"x pHYs   IDATx `T@=PD- *( xjh~E>m%*UZB/ڿTZZ \ jPA-( @9gv&! 잹s<;sI  $FI_vHHHHHH  b      $@A&            HJIyi       b      $@A&            HJIyi       b      $@A&            HJIyi       b      $@A&            HJIkvHݴo/C&֦`GЫSM$O(JOZ8Kgth۬J`>i!*3Wנ|Vj>WW|'j{=լ Q,5 .url]n~>=H'4H#PCZc2g> .tuAf"܄xV8|21jښK6Kg-]g| 'M1ax$j]}Ք$@$ LZ8 @mh&=\ũh}!&/߆{oRGM{PVV=6W s$_Z\jCW H$F jHkG4tSuѿo(&$@$p, >.F@fv˫aguű]"SYof͚!W[t%a@( U1`X]$zjLHg+f$@Q4ð{()" 0 H6eQkl]兵QԬ٢-6+7|ŒQ !p3!H#VEk4}qa^H1,޺KWc[Qvލ1|$n L5//Cзns/ŚEK}~G*!W[S,˯rthqN? 22.F[~4"7\1!Qk=⿱썷 rOK/ˇRl]1J&' tFv,?lFGw7܈" bBv‘VT1]{2ɗ'$G\k%nXmش=|i~?ny{y<~}×u`MY[|Vtu鮭\^,gh|k,2'W,Ǣŋfgp\嗍w􈺳 `|^\xeȸgg)U1$@$PC%: 8Z k˼2V-)5^ g4o=|Uy͜[Ǵ+cz(j%rя`h唬BzKpd;0Zi`˓ Ie8I‘*vO0?Nm% 8"+وusDŽΑ=s7F1Q1wLo]4{G՟N"昱eY?j U=-¾Us9V7}q*Ɲ_8imZ4Up{t~CUq [G$@ %@AP,HJea{ns&r廰13rqeߊ"mkg`h"YQ+`HwsFKcn(ysQs+X,09 7UQ]Q kv'~I)ߊZІ̚/7I; c"lqLȗ^k[f-M^ =/{1>2mQc"&619y;w:/d&L [BF`vdX$ (b+d!o|#/wJsĨ&T0G$|N1-ǁ N+W(\^ z]Nʣp9-w 1Ȗ1Y&Uֻ ߽2 ot9۰lG9{WMC*tÖ-cb*+#IHju @sϟ\4of29ynpɒ`AϗY9NJX]m_dNwE r~1N('Gmr31v\~?+BbLWcwF+-"9G+:{i+[DB@q"]ވt%#7X'b}I&Klsܣpecຈ;%j_}3H$@DT.% Ď0 ϪDLBVE#Bq<%kM1sE 5}A^y Y31A/=ꮤ0:@p~x=ڹwM,\]71~jR䏼F<;? јtjYa)@xZTɈv5Bx؉Y+5{W%^I%T+ߘވvS1ge@~RoFrh{~SfD+\2&Giܶ#bdLo[q0W_P|=kc;M8;|.gcDDkJt: sy E okg[^;Wᮛww(y}c _9غuvoߎ~)3l޴,IJh9*Կf7 otD_|CpDʰ{GǦzq!N&M&.9s1|pw! H2"Vۡ7EEDTT1]OqYqjͺ5W5OOUu8Żu.:W9l*X UU~]j,H̥6CN=0sq}ុg~oUY Jb1r|UeUE3̙2㽡pB]^n1꫽\ԄUab̤ވ<KpEU D% .88 kYELm8nM*c&<9o"̖+oYB5is+|ܚ,e`KHL'foI vGs|.ܰ>_x i-1>p5 oc[͔-ӛo$}c~l;>}Od9׷s,߳;)6md!+m fsY6 x p˴$@$=%?&F1Ǘ6}s)V|U^\*wpƹbN]^;jsҪk+ބE bѢEXp^U@f$x(RQֆ*/%(-XvV̱Xyx>+FSac!Ye~aWU]^ruG/+!?Tvǽ?UnT 5ĵ k15s)CQФE7 i WE#+)4K+l˱}W}D} 輷ly?gfbȔ / K'U: _xAYlPL[*/&cg(vΧAa7 ߈|ud-g3OQ8\Z.&mO/I_S`Š5<"$@  cT#$<ŷw"j?9ofE;c& %͗;/2}9ۆ\\?; 'X@|& 6^%.ߺzW2qI{W=Z[9zyr1'q*Ƙ}kб]+Nы[3J55Jǰa[>ޏhF<(Rd/Mo$eo%։OJ+vI1]5w/3$@$p PEجH 1}b&͛ٳ+[vߨ!h{>=cL LO|0_yާunZ+ݻD2L-Es";X/| [̜3[@v Ul_mS;\e0?W֫pc&cg#gwAx3;5?珧]%v/6.A} g`{5楼p# :[a,^8}8%=3}]QǵҫwZci-xh OΎ>޼_:$_бӻ?|˱}F!y*h;~Nyfl)ڃ6ڋiCa2Y(z#|Wme (3 @4q4[Ƒ@UDoQy$1Dw)ST*\z`֡d$@$`2`,HHHס[nܹ;G}ɦ=>[ $@$Ps5gHHHH t0FҬAvT<3G/ Ԝq   Hjm ߈.:\ W`ص}+֮ZB#Q衕5k4zIH 廏vwX> Q'k IDAT>y9>Su>f{Q1햾QE$@GWgB$@$@@YyZԽ'^Ƃ<*SlF\|6V iK$@ L'"   G|Vj}Rc'>m|,[Ze@ !2r\v%# &@A\g n쬔HHHHHH P`$@$@$@$@$@$@uBNR      &@A\g  :JIHHHHHq]O$@$@$@$@$@$P'(;+%     ku}X? @ 쬔HHHHHH P`$@$@$@$@$@$@uBNR      &@A\g  :JIHHHHHq]O$@$@$@$@$@$P'(;+%     ku}X? @ 쬔HHHHHH P`$@$@$@$@$@$@uBNR      &@A\g  :JIHHHHHq]O$@$@$@$@$@$P'(;+%     ku}X? @ 쬔HHHHHH uX? @" 68V|TcF @J   @$H R~7H#ӢU,1$@$@$P}^k5j]qXH YP'˙f?IxŮ`+v ֣Y7d$@$@$P]^k*կ/cu= @r N^@xgz<|iZͫGV4&  ("_uz/iܸq(M G)Q$@IL8O>NXkz,//7O?߰h"l۶ _~eh8Z#  }ܹ3 n]v5XqjjjhXmթh# KؒH bXbxpB|G^^zO<   N@^}شiy 2wuF-[cVk()b$P I[$@&_Ჲ2#pu!`ʔ)8]=#  عs'n&,Y| ѤIR+5I|1s*XbX}p)ٳ)kIHjXxbvmk?ݫW/7h޼>ػ}t %2]+XY( 4L@KEqii),X`5 $@$ ȫ?:+*Yٰ$@aa@R+u˴꫸ ;O$@$0 |{+bZbN1 @r Nޓ!`)+vҠ+8 } 44K3'CYQ $qby4hpV {Wu{ntԩ &  O<{ ۿqzԿ#$@$@k(_U KRRRHHH4iw:`ĵ@"`Ws 'l- T$BX-R!+b @2 N>@1{X't$@$@$ XAla(ncIcN8؉NK't$@$@$ ʰf# Ps @+Dx}  caGwOa: &@A矽'C_9Q! 8X!l7Hf(kƏI#`WH>: $+OG$@$@> 3bHH0}k'M' N0PG N3~ ؿiH"$@$` P[< $Ԥ-;K$@$@$@$@$pDG;™HII-Q5"($pX/G$@$<6{L׬^^/-5ZZjcnTzEG++Vn5Ǽ$pܼشkc @ۗIrR}d=tom^o$P5rգ٣۱f#Z:q4*#    $%`Ez<| yӑ@u XTڗq&N1G*){hO$@$@$@ "PVVիWcÆ hGpظi#ap(=piMТUsh[k׮sZ:7z;[NPXL2+zˍ ޱc~_bݺuX~=4j@ӦMѧOVz$Ys =Vj.d;{C7s[n߽{w:lw'(VHV) > @xܧg傎,)ÙMQi-d\WKt}bۻ;wʣqnƈ^| .R*  TE *uX_|_/.-FcPFRo7VM(48unu4IKŠ /ׅ/m۶feI&hfw7TWSC   H$]xꩧ@~n.$m}SbM\ˍPѡ. ~:vw?4F{FM:H +ʰp?.d\ |jǤWך8ybQb~ 9_g~??&λx衇S/J"c}VI$@$@$@$@/q :SE:q] خh4lNEl'Mi[Тc^`9ڴO[a^B^ϩ曑sOufsH!:TXʰ}Cne:Gu%F{    j*~bϡ/qΎteXEq#gJf's2MvN9HФϮX{kfSЦC\sX9 SN|Oz:7ҲXW 4s5hjHsVtwYnh_ڸٽ+*n?XQ4P+} IHHH 6zp(.\\fp 6:@'j b٦[ 1M)n69] V@#A\sY/1r`4δMָϗa܃=yOܤ-i b{C-* 2슦[:̈Uil̯TƏbYMǩY#(( Tmp `_]=jo4"W&L3UlW-u_|tfPqa_*CbǍtxu$zLq67d7o\,?';ݹӊa?ڑ8^R#  Hza~ :`&`*ZpUa@EEV3حѩF["%\-ZZ*7~+Mu˴1K6 א:ea hM*5 h?l;CU uk-EHisDݴ p ś_251-iXG72:tGӈx ovc1?@{ ٿn:f κ2Jwsx۱ow1c WHA\Z%  H:OzS#~֭h>=BXNaӪIVڕbͣkH ["Qz8g۴q*&< fk*VS;F44N ek+Fa-E&skyLQzWC 4Gtm|cզiƩy5/c%+|gŹ?T\zz }8q9g_ !Q;­篪vCZ^IR4%   $!/ 7JYabVũ _}5XFJ`ӗrE))l7i̔㤆ߝNm&֭ϖ[ndyrTTh;5Nz Z5Mk=IrM/0je XW=zҎreƒ%K('UݻwǼ?s 16>fD quўHHH )|J7<9{t0"Q;n Ouд~06U=:~NWA.MgUٿ+VtǪn{ּFb&AZii.ެHWO&#\-Ӯh: ] cӲM+\Kwj9[hXՕ:Q{$-M||}8еkWl۾]Ηiգj ,36cy*QW·$@$@$@IHÁŸ/2ӯbWwVXB"OElMN3yujvku2v'"Ċ]'y7q*ͣvh!5콩"Xۮvzuv?$7&`~$xcJ>IcNǦ}yF8ƙWqdzc (UGQF8?rq;:E'6 *J0HHH x/fVIU*l5ZX'ba *.C/QYIVamq_㜰W?rrMZǮQP-'|ղlW: ;mtS5a{6j&>vo]0[u$/RN+MīQS"Wn ^ןlH%/*j?CA SHHH X yhˆB]ӕZG 0J*(ک4㗼!JdSM S++WtWkaAkc)OriR~76iUè>^ЅϝD'#vEX6^uNGyO *tUa#< ʺ"Y8b°CJquG?)K ;i8#9G5 8$@$@$@I`߾}\EF0:+b#MX:_eT=ꕓveV kymV[:T@F;6sҳwJ a#vU`vڮVFA#7 }9;K/.YE (:z"yi'VW\5N]Z_d;&d975g_4&_F @2SNI8O#jNύ +f5U&+DH(`Yú-وLW*au*L ݞi>ǩ;! Q27В|zTlIs}*C72e;w֚nM/!L4 g/|Ɋpr=)^Gi1 P͌s;$l":$53yl8V~a~xZ _#Yk?Oy>lIs:|X$~Kﶫb0]$Vn}#c>V8>FӼbœѲϑv;ƪ_UN_>H$@$@$@ O,)N/*jeiwvmѺ::uM;\{)LrF{ੜ' Vqbݻ RS`ip_g4ey8ߖi FG9uh㑃DmNӢF뗱ءZ]._yh'R?FJ xk_$9:毴GN 62f   8Vzg1k>l-XRBSgQ^*lT 4F8+*LȺm X#ݺR*^SHqZ8WXk[ՕFʮn֔r)WW*U[uVn7ztj(e' _/ڵkѷo_C'MWnO-/igi 1pK߯ 9߬_>LDC÷ri5q|x݀ 둮jݛY^~~Oakaxk4WݫjYPW IHH%K.r#'y]&{:TSlg"8MU\?Y0y|Q*L),Vu" _H5--!Q+hgڿ2e:OrgW(uZű\iz/o<IP$޾}s:[ mcy7=GMqpڬ78Gu`\ׯau6{O\I+3h+&d4ݕ>τ=:|7 S_Yw 1 AWӀFYqj*pݍf%Wf:Slq"sDuN FvE> q#gyʰ#CX9)Dh#TܪE k 8ifZ3_u9@IToWmXm4ql;v/8, ]NIvax9zL+s6~Rdߏ0Y)6FxD|v1cTzmqQ٭n;6/y;=&KA,HHHax''O&y:9N9 ja*zC6eTlu#!%7˲ٮif8Xn8MIzͯ#m%,QJ'UkMN?l9875%MhlݧeapnN!df=G7YW-wqH   E@o2ǓI=cAdhkd&Be+*pu5XsN8#X5݈_)+ڇVDՔ྅Ű ]YM a.wl>kMP'4k8iLe&@Z4t9D8)N [YLVa$eowBzSisj2WCofx)Bk?oQOc@ed77tڟdxl~RT9tm+{*;o{Fcea(v7M$7t~g"⍍. aZjH=Ipk̈ >cF䟊Rlon?U18S,GXʻ1$Q&J,1n:) ț)FF,jo7b%QC94Qm0wmf%Yicucho %b)ˈb1XmV[4qV6Z\i,0+>)pxlhSis}miW_ESND\Sxh^ny svbŒ˺aL_9zEw'ܳ#1m8eT;U/?ϯ+smϓI^BY wmXVZTOgp.bM~f~Gրɞ&NLH vT[(?#d˭{+*!1,ۉsc}Wgm`gYƘoѣNM'X5RN? S&"-ڟ>DXk~LֹyM{6^mU a6>{tn-0X~OG|iJPY1i\$F=uڥ+rxsuO5 jg%\fy5 Lx~:V|RUϿQ<Š4o7S_ޖ0/b{[])N:6=Cޒ+ؤ1\񚿊o\@zz::m lQ]Ens?)kR/o”QWc)u|s5W x^!(]?W6^>8Sn5m!.3'j[WKW5 &gFg̘%ϴ5lV^ 0?Xo,hu̘~=1;sd GƍHHH86||L> Lյ23mgq L::'t<_`8er -~߈[|b `ͧWZޡZ&#uڤKgDV߶]L5x.8,V[{[Mpc7q.N*Q-bpȦ};tׯU2|?=+< .{,:͂1{`C}E8d13 >?}1޹? -ޖ1(<|. c},Fw cW}goJ/<"x_醓v-?ڋAC:aC'wJco߈AEm0yXe yA߿C0?aZTjU?~Jqa8|!8j} @ C{7ͦK{}RU21|J]OԱC9m PDŽq1hrLcRZ2@AF.ɓb/|_:Y{e?Ĝ c߽X27Seb1~LYOC%wx[WbM{FXgƏgaIq^m8 g+~QjVkƙkghvO0P|`4?UrQѕqF(#>NE Gp%ޯII//^sC{^MZ[OTxay^+*$125Zjm~WH4x5W;w+at]*~94h[\V8Յ` /YZ7p6q[~3{\d:]6`:bɺbHˠak57b iO96cKI/ly[.g~/Zhw h4 #d3.<cҢ2>Ę(߼#ȪYq> ~&%tùNBvݱ[7loz}=WNJ% ^SKVbS w&y< 羆_CA&cN66|p: sL(Obsi{ȝV8壎"ŽA ;(c1aRVgd< MEy;Yl.g"cjy|w΅ eC=[U]y7ZE:w أ3x&]򘴣_JO  $FifW9Sjwe؝ j|(xn*euDz+{E?\uaOMo]G^ǯy/m*q&%1Oʌ@.h/i63~ӀMLg7қy>Ѵ)t;SnKo i06ɛS9I.e3e.a@(-T]Ӳx4~կĹ̪k_˲(/) |]+}ѷmS `@GeicG17Gz"?!)B|OƆ[N#V"YTb"[Rrď ]iu?ήkFA/)ڑ S|}M[51"R;f'z4hgج#MBL5ee; Wg܍ OrC4"߃_?ʞ|x?^[8!U =jl̘[>+!^%_."ZRx|עWANA|9xY!5_{;/'꿄p6YNZ^VK@6۩5ƫ?">dg˶챶녢&|@fǾ@f"OBTimĤ}#q:6݇m~~ F?'?Z1h"W_!+iʒ`IбC*轟c?C7[4 ;˟ ;~3   8iK"X쪩_8:0>l* Չ>3oGetb)k@6c5n.sM;R&5p3{˗mkǛ[u-N-%oL__gqY"#;KKi3'Ӏ20}QR{5҃]A4I&N1vT=neTpYWSpI^Cqob%(ݿK;{NzW4+׼ѯ8G`Ua {ѩ牸ji>]T(J`:@1+8TӻwBɖv#ȧ7#p\"ˇ=5ݾ.۴Ek58[/ra~n#xKhqUhҶR׬ L[شb6$ۗ'uǍ>yتbuLןQjy,i \Y~tE ʥJ=nb*DĈ1ԩakI3m1|Ζ qGVfXZL6<^m揻s6⳦ #JY+Y4̼B=_rIYa7k/W@*]^r'A,Cr!4ӭ$Tz!AtMyPSBe8!rHT4iUۛ`Eډ,.Q*==]#cH~ ?7,%n8nhF d_\qX4 FS>܊>Cӿ`͵9F- oMЯ!J=k`3pZ&wf\2G}V܉s572vW.#9JA.\cͨ27r]"6Bq2)Xv~uiZC=.۾*GZt®凰k7Ho +b aWeh.mKrl߶eر}37vWE wRVϐ;j,%݅bX7y;thR[q| Ǖb(Kk \]razr_JX!;RѶmzXk$UVͺy4F_ȼ Ҥ[K0S2~z ۷GvкukȬ-e<7k&?!Uw7HHHHhѲ(s>t%T&q2qӕTuwS*EҩژRe"eS![/~ "@L92)N)M(R<0e U^K`͜cӰܩ'bKaSs/}"oZ/}frƊ3[}3=wjbZRK[ʡzs)qZ}Oۣ[cʟ9rGt]s۝ڢ:uje"nX6 |WөaoZ&#i^~_~iD1L5*0"͗):_Wp,M%ۤV]iwcz¥S1د IDATu[ ;y:u*+5^Q5ö m둋a-8dE̼ Cn^; :/T!!   @E(".$eF9[^̜_ݥ*UFI%adJ`3h}?45xֿ}by槸{WbAJwJ~.>d7'77W1讛p^%\+;DZgWĀ?Ÿ:2&>[Ͷ|VW]ݑjY9ֵbC)BJ-u0-\dl1t=kYpJ&ٻ6veatRU{kb5jbKLK4hcL1 DKg;̜.+m {fig2L2\<(lD Huz9SRq43ljy뫼vNKpGpe^7 B6 @>$_Y%=d0+5m{;Aהcqe ډQ:^?(S 13P*]J9Z+Y&ӷw~>u扁CGhSey;ʲi.H|8oo*Mdn[YLIIϾx $t6eڦM'ğ90pt ͬ3! t}gUT I@Q2>m7P9!nMEÃ,DztWkzs7^#8#4+d v~d:.g/AϢfho߾3m>z ZC0ʣAPyJI]#8Wf\_ S$e1ZG+rapX:&/i/elB?}tfSB *#;>Hb塴,;Xr('rLOөJK4:dy̗Kt^H cjNIi/24Ѝ(~Yp[4@%aNߕbTʃ+A@H`nĉr.^^9'͇8#8͈a9urTԣ-?;g3!`WBMrܧP_-’1O' \DJ,OYOkWBLײV˄ )q&QV8cdcq"!ֈ:Iv-Z)$("F5o=[9L 5;}>D yZъ$ eB 'uZ p?ӟ!]2\?XS:?GWNUahJ'uZ><'W8#8͏@q^r=*d2}:'h#3Qt@ۜO4pSsa(GBWy|R1,-L)H6[aEbujY(ӥ,;ITf}eEwV_"}6cD;:$A%Ď?;rHlO_dڪy.;Gt8PIM)&w ~dx/vt _Dy.?BL'?}!>䮷 ]˨Ћ;#8Ca%8O[г=v^;ߟ_'n9מom0`~KI)Kj/)1  _h%RSKDt(o/CaBHN$@8f\JP!(]dkTJt%LX6Q&=%̏,$t"D؞`Lk 'LbΛ n0>1,Wɴr{=KMae9ѵH]]}/ϥ^ -$idXJP拰/mՖům;!F-g:;rFLro 0W]ÖrHw{>x1GpG`!usbPi惟ynW>M|\ өSB=N\7o!Qf4SA0jPd\(uzmo0fFc%l`H G+ SLp'Vb֬i,,u*+7rqq R.mg*_l?(|;;AnR~O}Nǵr]Fbr, uJhʝXgc_eaq++oyi!Hdxw*Y2? ]W>L]\0Z?Wu>MsBм#8#?'VbZȸ3b;wI }g ӆv@;r*GB1cNda.0HrGǎj! )*VL kcb{2EZZ˟(֓-buv1)W)"kHt1S a!X0@hQLHӉ0f\y'/-F 44:;D2\SS,wh1VLP ėS?gH1Qވm"[2)%pZBJʯ}nu]~:fp>˯lㄸIy!GpGX_'Ӣ/zK̯+8L;j]g <\W8fb+xntxwǫw:y긛XN$ 2a*%yWӵLLyX_ašȰM ~ j3Jˮd'i6Us|(Ǎjmڤ֐:5 s< I}},aaMM$Fi%NbN6%#G\UKkBPDE9K R18%ɷt*85,/V}'ӤxNI0oRLTn(uueU^Y~ 'čAeGpG`#P58V 14{OUbcHitr; a I} Ci9FqnvMHIhpԤ (J@I(YHm4'1'+9NuO,RmH$gifHf)˴@YDz !~}viuoIzi 6KYwB. 1t*͏arՑQ"OJ|kdF7s[YyWkߐ5 ؾJ bבM~U>,G'W8#8nc>㠛zt4mnnĵlja>=y(AI~(zɉ/&$)H"3M;"D?J+CbUgkنa2ɜR_he{c2N˿@ɵz눴ei>|HIN5xɸ*6#Io1vB`$$ȆJQQI:*09U&6yɯW,å MW1d\Uyo9PW]W^2#6kk~"<'̓8#8Y;u~5Z*7-B\k0$p*Q P}NmEB|WgP$4aqGb˹cD^`kpkdeZykغ]S"tSN}I3Q)rO47 쌜Pߚo>rjx5ĿM˯넸ѐyGpGXL_P[)CW!s񴍑IIIԸ6EC]bإW{r>:}=O37:Lf]RLNg& Ɗ%Ϧke,- q %JC>yReϰafvF5]cr$SK!-ŒoS5]2k$.zL>XYsG8Ezv$$|cdbE##bXյ͗4"B%1bk?QS44WQ ,G2.RoX(=\ H_]ougof;!n GpGhjE7=㾬ȮEֈn(m ($39^& Ē9$Hpfq(vo1뜮Rw@a= 9䀟W ,k^Nyo(I5ʴp #iR IBϔt(rc$tAɩޗͶ`)&eɾ/iF)C 5;vS򁄗!)68\eBp +7[ڙ2,c8l&aʕKHpYc{(Tn?I 0әiuU>ߵ ;!^K#8#=,ƭmb .#~]ZGv2Sm̦0CC!qf(XVh<&%uށ%Bqح?G;X*E@s3}|ӚNU%%GFI2$ک8Z⡿f/q+ղ:V_9*Gl)i-W9)S\Uu޸jφ~Gۦ^ٍE`?зK5%y1LYu_/ߡ0Oܣ5|R/쏖~\z]KczV~Zn-::!n.$GpGX#f-Kp9~/ᢼ, /+ƥv 8L1jV)+0vc(Y*s鱜gڻ_-.wԞ͏af?d*89KI%" /2,O2I(q95m+}eSHQ˫ǣf9ƙ-nbkXDTn$=ӃXW0{(83fxNJEya'&c>|8{% ~WAPZjDE2L3uPCB8=>KfNz~i脸yIGpGXCHx .ǿ>Y] q2,+OjW7Ҙ %8l]e]Ϲl!Lap4<2=~Y%RIQ,wPޱ}"{lOu%ѩ$U ZGpG`UUIw5dM1ZI`2lIi$,D;$_eΨ8ȇ "T@y{EJ芋k/NzMa}$:,D2f%ZqN)hV2E:tGNNu~-.@e72$&gF?V~KGLYu2\h?`fĮ;.ʺ"HF,6* :CLѬ`8I3R jJ ٘9s&ze xwѽC{O.:{7/^#8#/ ࿋UxX8dQDkp k~%UH%]c8X_ɡB\+jCfXKr pM2L[!wio=>F&֕F QRHK![$$74IrNjBDPXSCRX[Fhzyygq88k AhaEFKrZX)O}nZڴ 3,#dknmũ'xGݷÊa֩RNa%c%$lq)An};6+ip @ 1%zb0;$h{")tW}f0mׄڈ/˱yYrywPT_t1Eܵ4VPc9󨻌tqabEAێGt/>Y P+3DZB%شKގزWrM7rӼW q9#8-Uu+ZʖȣdmEcz!_sU'VYr bL¨"],%02͒'ɨcgR&njUh;h˪V~?Woy ;U`E 0;kPj+AD5}CFVCM(x$zyR 8cf:XY 0O !bT@yNcƛ࿷ޚhN:-SlmdjѰ035?P~Aˠ˷aYA nZOJutB\Hz=#8@ @YKuǧ.f 0v!_ N6*XDFtْǘX ,&а:gqey \v4UT4P|rFd3KqHeggG7G1gD&y2+q`,f)0uڬ־Nb^D9[]J;iȯ&i7KLc?ZIk(U8u,^^AܵHHLw!mme<*%AJV_Ռ q3U9#8#p᭹WLEh^(RBGɁ/I\AQwtK9 |L U"enaGiM҆ ]\~={=uBz Jôh/f)D!cM'rzbb?sOI)^D+\ɣi%R3-F䥀+TO''{ k/ʝ,#uCdLS]CMʰ [P?-էUOZ7Wֺx @rGpG%"WQ? *-J0` >ydje%Y5V[(0"I<䓚lR(8 }/C gxC/ũC; ?'j[W?5~pԅ8/)UW IDATCb-Vv:e'V[g2[D憾Q23. Ɍ(n!?s4/SeJBg$ /9:ȅI5x[bk~m>O46ղLTu$LdOA1i⼼DŽ_d G$%z* wB*t<pGha,Y?dYKj1Zh#J8"6歏Dqp FfLHsLhU(m_nK˱%WBeͯ&sM=S0Yx+) iy?nE /ɮN6,vL\2@ Q}MZ9M a @(Hm*Y%doKX [2b1 cqNЃ- 1ʗ-n>a+g~<.ެ :իtGpiLSK|+n1@O#!BKV%%@2X&%iTy"ZZ)$A.a.Kd$"}'}؉kge IKm)eur?䇨UCoՂK[*K|]kD[`+! *pED0 XoJlӞ2L|kC8Y:}'4 Z{٤mwׂHD%N3ݷtñXY;~Q}&DpBhȼ#8#q P)&?M^dM[Psv6XI0 VI\IIcf RD lj%|!Kl W0?da#VWɉEu1!W9f3Wgf殼J7< BKYqmq蕔`_$dS}NGdfx!& T6-Fmp"3 1˓#Vmˑ䘾\OZ\7-y0m}ZuTX#![>48YrVFX^^\'Ç~ ďjk&WX焸#8# G*\|b2ұ-!$w9аx|a-q 4$,u\ӱ. Z,-J%$XӥcX>_ҫdT)vI)2jc{2ҋ/Epa∻Fa=m}jaÒa:J!ica:xۀIx2E,,Kqb\?,vnh-Nw- ??Ww9s>bG⛌̳^k9D:b 5YH[RZaZMBk/3c`*o %?;OXW37&J kj$6䇄`D]o­ןO(uHjk;!^\pG!P+b R|(-I"Lsqjxqj$*rT%6lY%iNZ'˘u3edp5khFstlޑP*Ǹ"5(?X {oRq}R, B:-v(wW_| W\u9nfth ;|3Է笃VJG| ތ0gbc⒎[\ݥ#$5fK]*65?t|?4'^C,lFI6)--]=/}Ǩ'9޴pT$H$IZ<\:/01!L㴑W-o>̅yxy\tX1RlYŨjk9!^\pG 0S"ߛmi .>$Sj`[ȩQ_Bd# @2е $g5Qڈ0zIᴿ(Tpp=P3IO;gwyt[.kerpaP$1K_>A Br~,g~g,,byxW_ⷿsLFpH.Cpnn~k<'y{y -;?G G~t޹N:>%Vb)u?0YN.8#lp3ᆷYKumúX ӘI8 L4T$au2DXBH)#9e@Y J$4c7߄YHL iX΃6#r!.2[]qe_/1-Kc벶U޹XVVYҷzkx}ݸݱwD!eJPy~e QVaISZ1Q`8n|o.z}yƙQP^~h`6 #mڴQ]ܹ3NӋ/FEE*++QUU%ꪑ% t@»"+,,D~~~6m?4Yy;GpG!٢n|X# jmJtp&h~ \DАVfd*L2˰p0wI'Cͷ܌_ t\1 Cﭻk1XR 1H`"h|$,ŸO„]B>}X;G AȈ NG50IHuu_%a _DBl8@0dm!&?9sYg;~ Opq $zG4Q!QNbMGh i=,#FG#č%\)/8#G\R+qXtK$ lHUxOoM&5aFJj) Y<[$ ׳cڂJL"UE N߻7fnZ,;4tmOǁک{Hsض *nhS'xe0![ ~ k_>oG}]};VItG -$uuuJD6LBbd qA  T0RLBL1L3BL"2̾:!^+eGpud g.mq2t+ZXaA$ #E]& :2ͼF9!)Ncy)HhkX %1O=$ˡ.l~W\{L>l`;rfr{Exim=>_\7kF ~ux饗¿^sw>YtڣOJ6i/nlf$~5zI5em˧\,?^gm8]S{q8u7 #$$-d 0D7씽gFpm6VɰYㄸ#8##dfyeܹ6!id4OK#Q% ^ʚIp #ҁ|3ڶ$ E@pKQ3d-dyKjpfq^ݰiɺ芯ae5h/Ӧ7$AN;+Rd?'O'O+wE a%XT>mB-@aQ!z!GbblknC:U7'- k%<#'$#$œFM2lX r [YKwXFdͧ zcf^RY qcsyGpG` Kkb5U&P* f،7\6U&0V} ,byd-urGjg0\6&)^TS'낗5JqԦ&5?^M5Jrql( Ѡ8uD$ئJw rO=H1/u279!n j^pGh]wb 9Jo y0r3%/̈́ܒ*f5V.«Z e33C+I;lh,IҤ7Z`VKuXXS?]\.]E6ZG`FĂ>IJU؈0}sØw45K_tYg2N Gpk/k\<" Xu& =Y ĕiS+)0hʿ1i[@(G%BX)Âd(r3Ke#10kM7WaL槬 YcGۜuo fG 0R0=R&q"|נ%D}OZ&oa1T焸y9GpG,=< LkEb k0ЕK} %s4 Slhl1g0coGn&L6JˣϐY3!ܱVmZui_5MK'ow郑]%ם#| 0&!F߬–f˸;Ge9# S-NHNӄ&78/8# o~Q!$x.K s/)W#JjVZOf ^}^Ėyo !Ç?>ˤg"0O-W H:W+e$?#'#Y_Z 6x^%d.o*؏#Tp5,Hq KyXԷ,g:I9KkXq'čEGp f4_7ji-Fwk )Ӈt>ͫqh$l 2)Moc xewG͖ŘapS3Q]퇼ys~Q{݇\X- ;E`B/q&QVvK0_ ^t^gO=GX*o=t͛pFCGp b ~r\T]pV=epM}}Qm#-$kPQ2z/K#oſ*$uLt lwضG6BnTJQAh+dXi*yU ãɺdy_07Ț:{oR;h2~pGp6o,WpG`"0in.^ݷ},+CeΈ3L^ZLtjr!Ӕ*e!’Jk?k <|$zq&~_tx啷1}/=]=߲mԦeD`Iv :\,MI#U0mK 9J֫l$8#l8!8#8@3"0e^n~wMEy)L:tFf WNVfJ@PIiZ5 S-RݰUG+^'}2}x>ȉq N0peڢxHKXۗI0] [3ZdO5ҡ{K[(H~pGh^~#8EF?>5X'̭¨"1;JHUhQdIhZO}[`оSr*'ԩ(r?,6;|aPWmNBgI}#:* iǣ`ٿYӿzb\M{yhͫŇ7=Q[܄q.: 2yS]p#cę;/BIo|1{a?CNknszn%SVJl5۵+I9#8䵆Gp'q;p{t'b _aQ'I#3}ZmS0=9v(G>w!)%qBag uogFt-eKwš{/9#o7sg&cPB^ُb ,Vӣn &Kf)tUu=Ы_HpGp8!NaGpڿNk7ϗ5'm ruFKTlJ̣%y$!_ϼA'oVs+,ČO@ס|w,JHsa!jU8 CjږAvٞĶI@o$)Mֹ ~SЯspG:N8#lZ\+Pk]gdKKKy*;3^^$4 uĐ.)Y/'I{\y;y:<gxZ9ǟ4濉_?_Cۮ'uU~0Z&cM%ͩC:}{{,йsGpVB#8TH x7t u)ıCPV$Xɦa%)˯Q}iQQ~xTː V?Pv[AYwp⃓ЭWln53&x yZ! ;#mi;jVK:w滃\Z6kAB9#8@pB4ܼ#8> gg,kp[ig(Sk0kku1Iym4Z|`YN1etB~.aL!uKv>滘!ج[$Rpmб4K ߩ5hyKZ,T6˵`aw~2m:p`M$q8#8MC qpR#8j;'MoNȮS4sIv jkYiTeҰ"q6Dh%nkw"rO=]&Mtat͡M0(xϐ'˔d1=bH6Z#8#<SBroz-еv"fK#(//ǒ%Kh",^(,,OQQڵk߳oN|D?=m6TwFb!B0?ۮJIjSQ@y$e,Z&ZI5I# B~Glv#蒷N&U#Hzf[jZ+ump\8Go968\q3a…lgD~~~ೢeg! ٸxG1j2ٱ=>i+ђ"KRbfL+nj>G~>zcwQ?Q79MAe,7S4G1;}>i8қڬl5}x?WWWak5i/B/0YYYzct ee[(˝6˚TUUrR˩z@k\~z.W#_C`ܸqx}< lMO ݢXtJa e nm|ԙ&ŚLxW>.]q'*9ѣGRXT]ޟDrX0[; QI2Ѐ(}1~(\]&ʥMR!JcHt&#zkqF vwj!Ke*Կ^ҞncEO?U|wcc<zo FwC\d%:f^~_ա6DAnUTaֻ_|IMCey>?x9*nq>1|Gty%)'3vrz@jct=>خq>kN5_+Bl7KFMBa“d'8Re88YlK+X`&5c%XO됺 Yo1x{K.СC7{ NDi ,($k@IpeLְiV ׈+iFyVV*q^hM5E6%C Gيh . pk:8tCmtL0?O׿CE1=#q+pYB:EB壣ުG>#B:fO MO|Mzn/Oo9u'k~W5?~<.b̚5 }5%#=ӧ~_bM6=I9o[+B& s- >;;N2=p%$qG [Q0+ŌeÏ:8cxwpGQG-I`Ԣ='M< *Fau2Ԟ@627֪^} &8NRxɒq71bH\uϰ[7?bXGEPk0*_ <aH3q QOi+Kŕ2bq;K@ʥҔtQX}zG:uٞ =Z2z|ǖiǙgh} q$BHOϴi0r1xRM+}zdLrBܸko;9 sqb6FbJ>hQX~[֟@AUFTeH!T;!ʱyiWWUïy {t68œJ?i>˚eE$k{CN_tIi$> S)JYfKfyS%XMڵvxuB˓:%֭p2JWldm|W8̳` Gc'e'!N=Di ꭪O4jy>@^b DHHO[kDz>KN; 8_Y8!^8 *p|Ocî&ORC*)aRAO$*7 hCY/5ԉgܸc}Oʦ7~rzH zE2Mz@f~M5M,MVpPY/5N{?ޅ6mdμN7 YWq-* ]n CVk# ?J}kRGH@C^uq!G<(nH'4ɜm4] 5Ҏ3: +# - FGu&DY̬9v9~O⡇EnB禷(=R^tf4^Tj&J_i騝/: *Yֳfb >}TV_ڎ|8C1\!(I;޺>Fvj \Y:hghqA+g2I]PNa:&;eSuc}ߌfcǠ Hyud($^FNp!HJv{[x/w_+e󔅸rF\D5ڷ9Dt4~uc sZE^)Nq44=wIgg>13bX+Զb]ܴuXӢ+san]i!6rypq}ξq*tl$,xF b!&G*oXg[zPidLNh=ǽ}~Cp.P[ᙷ'Sf7E}2_*2_.uۛQ4{7|-'{I2lc̅XP162LߦM#u?dSр Li4iyLT$i4aCݤ^r#&1fˁ`t=B`8`L<ڭ~ 2!V'jH= L;$61EP~J0xzg#ٮ'I8Sdgk6ZwNZe8hP8"B2$mhIHq7 OcBd {P#>X+PYW3,66uuk)\ËqwvG߭zfUYSm3-5fZA7|Z*DG *ZPK8=C&_ȏL5?5l({30wW,lQ^z|O#-x uiZ1sቼvݺaM eWqkipsŷ_i?Ł8f˳ ;/<-ԧ`%t}nPdE9sfF \~JuгVm"Q=n"$$#H|Yo2)5xntkc| lߠ7K 161zxhL}cg^^ x{:~c G`J=YʻC1dC+O'7џŷ'9d<ɓȃbPF+JHi^k~MC k 26nSN;ms/AdȏPrOakHP>8] zǴxJe'cJ%Bz/iԳ}Z{(u8}emcƻ q˻yXQSȀufcۀ+d;Z-$34Xpl0x6 񐓄>%LtZ9/[{G[z?>=mIGk,1-LW+Q1$gU]{P_닪̠:ӁXiRoZJ҂=2|Xҹ+]'߿~LDQQ燖iEpG1ęF=l y:~1PoL q4s|zko }Z0$vMc*#@,X޽{G?|kLx) W%"X2 Xxiq`Î_#&"6noTIH偢̪\P[>=q\+pW4z >3}nT6*©úc~^ 1LO&A)Xl"MqM&lM0Ib9FE! + H:t鴕Eo[ftg7oꨑ0W F,"5HCN2$M^S`I[96c.)[r-)dG-!$KMHlʮG82ȈmHFYN?99 G>_?5"ş-VkFåvBNVk;M~pN_I$Y Ռ>}bYdCP\R`IM˦J[yr,i?clh~@ye}sٴ;G@Zexڔis-7Lz)ɴR,y-@+t@!J&&^" be~fڷtY;JO#8>'X~G/bl7?|2Y,7N7*4HI`:mea+rH,[:$S_鯍Kn1ra\ti4 {8K\y ߪG榫y,ZH pHsNJjd80C^oŸ~+QH?T& qEr!ݎ$t|-bZ4(Dve }@˄|vkbܵi=*mo3,>;cgK0K!߼ ] D0 JlբiRGh_Oe,MWXZKҚC AjA"PKg, f]ڔ4l:%"*<dZ6f.8_;!LjS1m\٢9rL\HlG}K!5dSԦJ[:̫kz}UpWJI=HL0}Y] KHԙm.lvc>}H}}>Cs%{n+70,=Ʃht|;~'7Q1ch}u~ɹ:lF:#0u|k}xBX<JQP>^˨jFcţR_EFX]>zᥕdO,>B|!>_;WBR&q IDAT̞;f}_֣gwwii v6;@^ ~6zH㷴uEзQ]nSB޳Msb"z342Oph5YF1_U-XR;^DãQ-0.h V{\9)eqVbJĵx}#i6U!`|vj=I$*nӨCpdM~򇃰ۮcwMaIܡ=Z6++ ]/ VDː+vcvxLf=\kX`lS4Uc:ᰁd!9#/[n;r^#i:LQ'LDzVj+odI>6OŒ}H$JcIy>#[)?ȱWn{{]aM#@qjsR/ *7œy pwgϞi_Nm֬Y1c`=_0xQTB1*誦Fsk^bM$1c뷖΋oIQ'd@s?vLnt[/2n?NRWt>I^jʘ:x}]dCq91\!<۳,id I3 6 [rȲkESb}pчޖ5zM$Z )$Y*滃8ep'L e9=x-4eVWѶDgR{. Wd*Z вa(#mj##CJ֎;|_ss XR\]5tI eLVv5Ȳʄ1CLSx8Z(iYʺ4.UwVW[eم.E@IQ{AAؑ(KlDIl1FM(v,hT `A w{wW@Pৼ}wfΜsf̙s̝kLbVg>]X+޳>Nj[y9]TH`8ḝ֎Hցp,ڪRCq+tG袁i2elʬ;?CDPorg6pY8v7w,. w넧Nkz'&nݺ .*5V2,]QMzĉZjx03qч{ V*Xt[BDzl$ʣWB4HI)9k JW0CV̌6\W$_]#nz7rJW gq|gFCfTN םԘFSaS xԄ8lZ4_fh :W[Q9YVr9.Mwأx 2|mȦύ)y.&yw;4*NKD|;C: n1 ^p'L ؓ-7vq!;Kjph1) ,Km7Z"qC0鬿[>H]6YȜB[RܔSg|8GޟR /SZvc]Aakʶ%qġA-1/p//}C#$+ ħR`~ߓf|o~j>3o7m*"->eH~m;5a{~?UtF+;OxE<5I WQ4^x,Nt@o{[ ^+EEpde*g2O%e^ǥ+GTs=:ʨk(&2PϱYy5Q|zᜳae/`a ԩ0Nc!u{ɒ8j7`uc؊aX[3ڒC>$Hz*Ҳr~3 nEY (K◗]>QʀuԪpbWS+L0q ПW[S_xB_8 WbW*W)2n{nl~k['ګ)q>$0cX*~:][Gp Ia _ !#?׉?L&q7_IKuJR 7Xm6Y!Mi82]?=LMUl7e8[8…p +2vgdm0[UU(9ugRƌ_}OkCiWqSJ#֧Lͷ4ǵrXYB;///[.cek!9Yu[yoç,dU6m=u>)÷jVKdWbb֡YL+ |Z ':3a0cpwiJ|d<|DB Nᄩl‘S V]I`ϋF]Â]FUUJlν5g?k$ȷ A4eͰPT8A g) \oO\bV$ޅ|WnRk"x h Q7-8PF1Q] h*NR_U&ʡ51*>i&4 VrԖ?'U,ԥo`*W&1X>^0lyuD#jcҹKgy2O`JS fal,3R!HxpYdi R; km] 7#i/ێ$P7ϧ,5./y赿ѵGԭe[`NN3 ٷ }JxshE\'LN+ Go[mg,ʋ`햶xS}.S5>Z ]*at ЊrtSq+Zq,k ۔Mlk5wXs*$BX`l[+-ZʩMm-qcomVww (PM-܄ǡBWoԲ15WcZQO4 !'؍hئT˦ICXt 6$qޢhp— o0"l1]Q_{OZ[۞Jjd5>V?'Z4@9N?;yΝ hSXPO&?P4s?$p)8lJ_?t=Z. r tF[=%L}$ m034*?[0NY+t`*>7]&nJ%+Z,_ea N vڿ9^{O?4Je P^æ%9Ic6= a8EcFSOv8 ndq 8~dm6uhS$H-G,"g%wI%J]S\]~Am5ɯ.  ~{F+VC`Mݝ̀F&. 2M =a"κ O8nHSS3xiyj OdF"\%Pil;uP;S2d qeXG'ck5\H*9-Ծ#?]7ob}!=m][SMZY.YEsRFq_O=K4ۆө|b&|_)L`Hkm4~A  V1l7%aJKTVT_RFKE\!g]0Q&mP}x#ׁ@JYU۹\<~Onv^:p:>~?RVoQEya8o{N5i' >O3͠t|}[j3]Ϣ `kۥ斾l^!@#Q"m5.@E⃹cf JO%S"duf̶EO^;}G4E|w3$@e .apf\)>;4@|uxWL/nF _;A.XpJ*#9 @Svֱ,E+*ug!d4i(|U/G,'/uZ`< p C[Ai\ k.u{FyT%;s{`"W8C"8 E5+]qXۧia+J_M}(tnMې۬S LD1%;(hoR GIܨIX>#ԟxAh{_7X?(KOoaa)K R|*#`$Jp/+RiI-LnѴ \3p=`jbv6A#ᰖo9;y:>OX`9?2dr&!>l+T8FCRƲx+ݲL1JBZӪ蒆maet'VuϞ%keภo1kL=&*߮O+&TE>b҈0sOwwƼ2_Jk\wLӇkiݴWa!+=2i5,L<5u`J'cBP_+]ge$XCyhO|sd?c FËch^hVPmwO2Z@yPV jnxbtDθ~{!;gqPcFc=w{*VgHTwկk0[a\2jߪae8h4Gא}dr:m"|rP=S{5_"-{|4x>GȬ `zX IDATa2Wy>` rwAV [$nt8n:UP̢U2al4`Z>HjjE8G};*mӂǭۻQUxO/\*WЋa;tj/8jrognQn9/?kU0ᢑ6}yWQ`RT@j\)>(,}Fd>݆FqĕoJc3nO@K;#k_w68u-H@dJRhWbZ># #?qwoTaK'?LNxM­>Iҍ(y[+c^k$U8Uf}o%OCKぜ7dWCcNgݭ͑{}mSh՟7fNN.= ^6^.z, ׌`,k"ҵMZS^H /5I1}T T2uAV[(!7he לCx: ՙh.sa}]<_XR ~p Ԭw jrLJ7jet9 Sxۻ :y>M]ТvNǵ^;jh"̞9< W' әNjGgz*d&&eк5e4օ5> co=F3*#X(Tl;5UlTr}-h=P/}A$O"U鲪VAK~Zm LTp&YI )?5O+R#ii[)4V Iʄ$ NZC~ѽ  !7BJA ǐ cBл|2ILǣH?;'>NjǷDFkb=vDI~`>3}#?{unfʖk')bе?p-7@V5W1+towAǟMCDIе[9ER%K"õ =s'`aY|ĝd]g|Ę&b L[霍3h٩0}OhFԩSQwomubb#pcX K»Ö<᫾k{zdԫd;A}eq#Ҏaz/i,g+q/.lQ_ۈ/a>o)4@(D\3ũ?UrhΕGbh "JLɤh. $ vJH{@)PmN'q, vAA0-e3HɁMT:wEUJoYaZiqkS N)E$úzN}KIDWΡQ|?>=gcOwi&a+e1&/020R,7}UX'L8q\dN\Y `mVUy^)aZB-^"u^?KO: {quP`҈ZiS'beܵRb4bMh2.`2M!IIfMmi[/G^gb̉б~ 6qDjYC=Cɨ{ǣIEH 2N˿]oKdVC ZmDw#2ut@%,óbC9عSF*c<]R\ķZP_GXd#}~x ȗ3DKW!;*,WX&oX*rQϹQD׳Q\Zh9yhP'/-AG LJTi#Mojd?2H6%=A*q>i/&DJ_2ng ph_{/鍞Ϩ$ֻd&]Sbq,ZG'<i.M~Q7b;oS5Np)k*N|ڥ |<+1Cv K1ނ i/]^{Nwz׿l.1*e~^tc.tYq?X X&TVA٧q0OV|UY9IGNW8• -t+)uFG^?(2TC03H:z'CQۿ\-ھbVmUHa+'¶JKwK[ *g.‹Lj"( R o drjo;.&t8ӷQog<_J>ZEp6埒o%kW7=6Rzi7qʧG>ELa 4*:'yiLo1gs矠G9Cs|*V ?OY'W݀ eKOfV zZ5H1-Db BpW_WS̘:өu#/~V6Yh7&¶ ϦkQwѴ-Z} Ųn72m,uLB忶\?9E|ìWŽr}\ȫM6Eú&LXŽYWߤ*4WMufiV`m$`R#n$S +\jFbi*|ѵkh1U~vhs.*U.*Ôr1_>,qucL.k_xynm۬/UR;P Flbc؊c0(c8Ǻ\Tm8eb8G2lŌ+:DtV!,j7T^`9֢!ZHD׹ꅾ'J~ 7]X .ӫ𦊣@O<7f >y UpBUC PYF7mP~bgdFj ogeeU?#7,;]0bûɂLjLBx~WꑯM8R퇨TkZb}[]we|u,ėDMtW4oMNV ƕ&X!OIC=]P8{}.2>VsYė{gbdyU|)lLGy<<(; !w O䗃|-\{yJ^_޿4o;oc3b1n9qR[]HmE7VMÕ} `nGr4|&@?+yl{4.[`,1}q&?zLYS`&^x{ \u-]un8)/Y5>&i{->6<9n \k?|b]',ɞ> kSeHXD4vcQ2|󓱊.]4yp_h|F twG>A]ЪJ^Ӥw7x_0𬕳pqwa`'ep;q^^Cpſ~sVF<]Џ /K!cGN7<9KSͫJeo66wnmmҫGq]P.hغ#Z|!aъ2Mq,|4KߊAICIt|&_-Jf;OƐjs~N쟢+ﻀ ''׉״6xdf,->gI/Yy \#4k,dmy^">eƶ'Ͱ4n $j`pbMqN9Lbn'.F[j<78_ܴ':7'aݸ{<\lW\~ : <8c;0̜zg5~çL i| ?pՄo^xs'q=$A1%570(ȯd1ócySyV O\(a'ӟ,~#Pl"OJqRrE1 <6m8߫5 Ф}Oic830b2>CYѶ07ko w ic$yxͽQ:A=A47ݸ잞Oc𛰫ۗyM$1nkMwߎ}\w^Fla VAԡ8ɰUXƮm4 ?w_mk%KKJPR\/'-s*ԮAK0%)w]@ xj:Y/>^o3FS~aWe(+^a߃ۮ9sVܔmk TdE_Z}++X! &2coVJhWxJ! }2qyW`ŊR,/[kTe3pY+[__ax'u99 ~]YhwV~{m.y[Sa{' hDo]c_H [(/W5kvБ(2<EF Ӎ+Xd'X_{㭱hWÁ'g6ftμc4N:;?~{ A;÷ 6sq~~r!^ywn=a>,{ᓱ`Omݳc/Fq*M.ŲO`Cgn C.Cxsyם8qw`39.;V>au:z d@8e-*j%[O}vc%aJx+7-қfb KhS{C#zj,:@^~" x>&=qc|>ʫI7Rqި\O^j<>;;޿ =!x un ßav ˑ[3Z.NxcY3ϒ:osMz}G'o.~}iȪO8x!J˗gϋ|7ʗ%e25}4 u)w>,B1k IſXb<Է' }r\qYq"dLY8Tg. Q2Tl!_G [#>d"iY546#M2w2~r86hRO<cH`x n?5 2ذ_BQV2>C^?C/ŀE-1p܃_Q0`8xEY1ͷ纥9{q͛ilH'3֟,Q\<%lM} 6j8~v1[Й+oG\xk"& >sc /7Ȼ0lR 捹}.a?׼ec=Cbؐ2z za:a,.5 w]4NC`!8s[xԹVwO_F7Yq'k.e.0a[}>[t\f@RW M}Kȧ|Kgb>p HSbJfT*ӆ%]4un9=&ob)a+> 9+Wp:UU/ ѬJ9.yOa@I9JfIc'&l6g;Ű7>O:KwĨg/FBLmlX_E4Z6k5^&|`|wE SjCh`a/K|rS<Wa4˲j '?~.,*雮*!^c#}= r7aMpf T?yNLU344G M;QAtxS:%U௟\Z ]}Ͻ9>{gj}SiJQo ;n6res=y& c /!1 Mou?teqۃq pLO7˜yإG^0un(Iݪ"zz"XflOwE.oWq9]i8c(oZ0vs ?X0S8y{' ]ܓ6h>pj>n@1Cq(orfKރzJ{qal$2^F-[9mR-u?Ĉ|:tzrG^lMAzcS1Uڞ,|S K;U;W\wTXWضhT 2 DjqiXF-5Y}-16+I5w <2n4+:6^eҒo5]NNMރIr'3O)4WutUm>խ\5os8( >qpV"aO";<+!%lhIXsΜѡvX6کx3#ӂϩ{S۔b7c_N\=ӌ),>QܜlNѱ1ȶ@A5'&` U&$t lDEg0F"(|1nx[= )\f Ѥɥ\P(yEPI[&8џIee՚nxz9ňWݔx{8Т=C*dNdy_ lᩓQC`tΌX҉Fgj z9}9;(~`J` qB|9M 4щ![˗aKD4Qh|g|t(U`=`{UO|57!rYi1<sƿs{{\gtod:d5UbDxy˾/xRǵQΉ! Rsd'<%Ga@>s,|C4=Sx30f' M2D3j>(> W^0Ls&+}EYY-oWs2՜7ޏܭ.L$9 e 'vByʥ?u,<]oooה̷eQTߧ4WDɒȮÇIdtz9#Ld5Vz; Nz3^Isb܏쁆+9|%"|+W? anvk/GExUs~o~nRFLßlOќޟWwt8{ n TiCS:*'{6y^BVMሧUT8ѳ>r|z*/nnc)_2\ 8=_3,?ǴZPr+,^ 7 ;S("QNM[fcPÄfCg燲ʚo(V#w}DyqDDgظqR%0 )_SEp|o ɑ0G Ӽϴ}.㼹:!Z 姾8/')eA }U̿߁Q߮BMpP&*GM_VyLe VvSYT(UXf w֍Xg[VvNHƠ'eVe&aчS7)rx=.||) xe^.vt=γN-7S+y1gˏ!/N[h6;bBMt_!&KkrZUnIKP2"ߟZeq@%$E\gKp<tP RQM1'D]$Y|߲n,Յ]=db9^r[q„Va˒z"JNhGOmҏq@[CJWaoyo/\p3tt̚ ayn֕?aw{ {c4j I<_XylmӤL7^t>y~s̒} Pn80>'XYgcp:HGY̓z CqŁ!R2;t[}9 6vp,&|Ue O\4[kV|ݧo5rY(Q]A?7(lZlx?M6/z'rTLmP8 Zq- /~ɹ _1]{~ >W|ʇX[EGѦ,jlO_̩X>m*:]Gᶃd}x_Gq/_=ILEp 6ZO3v>d(5AמǠirWuI/S/wml/G nqf{a3ހ:潜:y-a~+~a/2 HNGK.Ix ˗K4OI_\&=zVʿM7ủvWD:|W%.3>Z\='quw\](t*du=ǵw~1_Voghx1]y(8\ʯ*f0Y`E1MKcxKgm,{* }%$Q&QJʁ}WUf lFgǕ3ChRbѺ}IW8NdmF!!DT@w!'grQvVG2ʭ76guc)T*s.,:)d I?Y41\VIc*\q)YdIF=z0OvQ4t~&yl|Rr7K m\$Wwoq)}9[q}Nh¬ۿ,<.!p+.g =ͩbebƑoc/Ó 8o| WA?܋9ݚܭ$.*՚a(=rSy-djt8–w&%Hɬ\ g fyQ-л iN9G>?^ UW/N5 7~rد3[mamWG*Ff~O朧wXU)g_ǯ=OԎsqo5e]WOLSu525D>q®v<\%EEq,qhNKt޴ƪK#w{I}{ ~yqʑhz̍"<w%t3sWbgccZW3m_Yybj^{|8s\hȪ|%V!  ;:νg9VXY2q0 ʷDL_b Oc5?63}vp֑O/ReYĿg:3qfj(LO'QCku4ϗݼFϐ^hsaH9 fqib@n#%O+&w/OEڂfSd+U(Obi8>fq\o~Ɋ+eio-n%Z6"Vڤ\"Ĵd<яiѯ WrZIUIƓ$*,v)G(S+ֿѐ'$a"yJi}x%>xsxxqv&O}UI}רWn_|-E\1E%Ω<`V ٹS~+x]R:ٰK%(aC~ބێ_C}w.J׽wGߵCϸ~n')ưRvr'V8^ &^ij11\:!TE~hPXc¤8vĩPFu_h>%G@8]Us"&ڙ@=!\nxȡ_q˰a0ࡊ'q/}冲. m—Ea|o%ٖ.XT^ T`8|[j2oPRZPyߖ]+Z4E|Y7'.he/r3O6)89EYN>qA#h"QSl?qŃ; Pry{B drcZcz2 ǴW+^9-xZVϳq}>suEZY5jPW烃RYd9/' \톬xP+z k$:W6̓BZjb '@kEEڊ`{lMꆁu oBA-UmU6zMADIA};gsbw,aT?EKZ]bW3,jūEs|Tt IJE墄Հ䔸Նܒ)V*m8Pƶm쌟k}RNt1n#N֩cvU{_l2L\&/mμAϣ* sRLtc5H\*G,26m>V7cj"j8S%#O]a pT#W5L\~U[[r(?Yңܳrj r9.?55v`;۹˸J8e ưf67m!ZvkN 4bd)8`7^"\XvXt(xZѮImda E:n/'# ^3VGySٛ1Rp4wc dEK69ʧ"h.tc> J^Ȣ4U\PJº4@b*S"|3=tH;|7cNDIq.(YD)H@Bը)xIdgCQAA1ޟ0p,z棲:_*M[a #1n*.ض-~ 9L|ܼpÖQMcp3id䗖ER/Rf)Ώ=+b!Kێxybb[vd#Thgś: PFӕà1XqnƸ(g2/(nsݐn2!]pKxB &נ" g|8.1/͈x'# лSΘ ưA7JJV-Q/e}qN]JIyqH1zJ-TZfMǂnλWڶiO+У&rl4nͪ&A +s3),z(#2`&aKDFdLdžxaT<w솁i7a $(X!P%Lٕi?ևEh_T^bT/3< uϲz޼j&>63^+Eg Fe#O˶Rzq=]:І[Xӱl1hH[Zy7&+Z$mZhVrZ8,\ MFa/ϸvV>li]W +G՗4&ry )g >yrD%6/Hcvcϛ5i9/2A9-r۵h.3[`こ+dGN6.gNQrJC)Dc)6\!*a&3_;|X0&,.S0vb @\ H[<)wi1Y%.|I)q4kj\ ,K ˼R)*r( 9g,]HL=:27`zY<.h1Sp nL&.K)V l,% 7ebOA$5„X_R6ؔK,r`57>hOp[Qx!O5tgaD]Lj 8#fq}&Z[[.;s)`s;s03~l;300S]D}*'$Zڦգ/o]P',gk%oPM ]t-RƆLU0GE(+P5}Ƃ`zѻʃ(.#:2pQdNs_t @9HL<6hzax,*WF c چm<#`ZPEK]AGStAj NT׆42 !38/?S]K~a0RCJ*BI>Cf( Iv A:9/z:цrO̷̙tp8MpZ8G$`r1?SlpY@.զA_m7f72z%%먴yuFkl2x7B~,؃,Ad +' m5)r\CK9Un.aDCu"nZpg^үq}#Lt8jQo^"m;RFUrjFI >,qU[[tAB\С_B2^H16p"33i+?0xK31@$z%Z@-Z`M`0TQty-B/EpZ8\ü.@,)#)7C5ICAN{BZ^D%?x^݊;c+vlɎP*].Ǿy{>gڵkOeg222(==nvoz3 ]GI'qb)AZqeJh52DŽMr9 QPi1%w'!C\=Hwe k1Z.㉮]0Kc8Ⱦ%tZlR 3JsE:巒һ\G}'ud<6ZolIR*tMtNZTb7ɻK҃?nb!:J 8qM/ۯJoؔZ˧cK:u=Sdi}ŖDs; Ǵit!Z WvYJߋ JG珬} qT v9_ֶMsϲ.0NBALJ~G{j,[oU>˒L ϫXx'n< egg\>۶mo}C#a%Jlq2_~\|ř$cڵh7a aI>\ s]|\7 W[*?\J%*w'sJͣ^Tw@+BӬ@|NYAf^~9eE?L[?=} qaY` @2wn 7Dr#2,ׇ(?Sw̐ 'j}'V-|\ sq^"-wIse=)@>̬:ʖHaLQ16u+WF7 _^CgE#QwTw^E"x+z̉nUwNjFիs1S3t}*KAhX)݅2_9_x~ue@1glBW3b '0:wmnWv@Ł[7#O8YJ OprղJy5n!g[ʤ?pc.yĬ{|/,rQSIQI#I Jj|3ˤAX<1,")B^b΂;tMqm %|v ¾_57C!ZbtKxJnfcao /C>`0B78nVWɓQ I`NF#w_@b)2Qa1&@ o *raKuKW@ɼ.|$c?shS1*qa|| ^WMWSWnR|c 6Cga<`Ǣ [ۤ&PǼS(ZWE'XzDͩFS)J<-Zlʚv. ` ҋ$ˣa1h=4\ \ bV^, 5o=.[,Z,Gy9 YYk2+޵f}0޾lb1.:[-y|₥!hPߣUo\4v:–$a/L]z0ϦH92 spB/WBY1UDg7la\@;ǡRg,KdԜ Rix0 5O_yn`qqgFLdE(&İP +eu-aj ~ E:<1gAE|K+il|>G+:KbZTFbїvB $GHAa;=6c+]^JBW18\Vx%`j!cd]ASZP|R^Mo Ӿ#M$0@LGHC^giUI1Yc.Ͷ9ҙoj7h.8Xpb?[ EeA/Kdh[9E3xø1ROѯ_,nݻٲp߷ +Tem1gp:3[ިbü:,p+^a?;lѯZKD䚗/L%+,'SId2B(ST{?f!nf![E{9b\ 7@&}~ +Řx7o)yb5^Dcy֞tUAD p̵axјNp> <xrF\,ȑ$8Ս W.'kp^715k/g0 &ƮFؕǧ2< ,&ς%+oPy1_R~ 6ӒzQGmk3hwNGeE?{ ^:̇>6زP`~{SEߣ?0TraTJ+1ti}QꗆCL uL7|)zb)bnb &8Qb2C:L%߆yQaj v#M{@dݲ [b+SqR\z͋yY&]r%4w0&:5<xVMjԕwmJÛAqZ7FPKȻ:|A `E._U`ipF?~5ftHjf: RL݈D:^Ay|E~Ge.{uu`@TҸR^pq vp˰#n͈d,UrTqeѼON/jOڕqg} 6gijLxPnrN5?g0;J' Rޙ+hF%ȗI0d~]Pc<E~lS:B_5S[D x4֗cE(D,6b0. &(}2g,ޟ|Oqh]Wб-pJI]*b'S 5ygC f%`MELTUxA30 W8$%H ;75t)PPi:/H$}sHOk-S/Nnu3-m%tOrbX 102 0i/ |.pjt,[Ci#b5]ԙҨuCխؾVoϢ ge%!aÆQ¯REvBEI: ,eA:q!E/ԟ;sf9fnW ge8ۍ=F Y@?Ivn"nۜA^S}s,QýMtÐ͕@0G+XE.sLprB`؝u{~W{H |]/lb_Vs9"& ' LN\.,\&:3{ &hĸEMLNÂV(n$;ިQ#~pzU`P`eC~\;\Z0T{D%>`& [@=$S` (dס0/3~_ӓ `uabV=#Đ~s!⦛ ak/\&V_!c| L԰+*no1%7A0Ē!ʝmtG+AǤ;_/Z O]n?֬ rˁ 9dj{׿Fƥv=nޣ ?5nR["ɐ 1!K#)]ۇ uҊ H^:!=pRo ^R|AW75_\J|Lu9dwF??"%೓'gϝ4~;i U[YTۮ#^ω!WMjӱhֺtWѥ]R 7Y]+r+=js=n|zBV>oЧ ̒P X%>Q.LD5IcNp=9/]]dD[Qdn|)eJІVjmA率VlNdlʼ}IrM|ZO@Mu̘Fþ>&Cv IDATׄj CM6*\ۅ+\kI:|U?˭|gU((=$LtW,8.8?HGU^~ tyRofOzlpF0`hU`Y sL9lB0peYz&nssi iYE=xٰg'i\x4/q.#QTL~ 0LK 𹭻o%ojaPi烶+ʈ*jYGР9!r$Y=,R41mz۰#&J4H ٲ ޕ7 4IF-ӂqҩNm8VYĠ@cmYnXR$@Yy/TH@C%#w-jt(f@]@0' k5rm`X$a8~t-O@C 4 pi)v@g4I))VZGI<$㺂.Z~ŕi|UJƍ88^JlQ4|qbc2] O:/R%e]DG?D2IĠ:/ƭ  GCP AqiuhdCsA}Ns^I7.F2- sX7  i&|hRvujɖWVተZ/*"})h 0M:fYARI_ƿk]gN7|.l]5iԘw^hБGӷN!7=}5u p8Xre KI^>k,kn&UG<PRYїุ`>h7hq+oKAx(x?=3ż[hԛ7jEXq$`s{ ^#K\eǤ B sVJs!vMի#f)?|yK>" a!FYB!QĹ<|"8ݗ+@X?N]}yS{. c DXĜaZ|J%u 70e7)@ Yk%Z鐨btx`rj!چ8O7 mԦ@xk.ixjж>lwE9jj5Qke˓" znMg66 n`l 4an?XqX0ǥ,A[&.;3wؿCO=|~Xcg_YDj\N8?ͅ R _2xd2u |?*X׺Z>ڀ_x3?= GR΁O'{aJ 0vܡoNK'ieh~8%:N;1R"Mv'jqIE>~ aNx6@6j;q΢b`%ݭ{?aVmE.K&ͧ&5=jf&;/83igci߾qU' ij>@eU^UFt[B&~Hz*VvF ك7Gp4::w˓^ |sf(oyV~?ӎ{EwQ٪:;E&.AaG+չx~?:i)ԳgOi?;>Z9wy:jCYPT|<`Ͱx EavnR 1V6L : +ѧ~ǛMd0(,ZԠ'46mnoWn]jGyܻU({2gސ@V8k08l\t섷@K#p"HXQXQfWޮC*_ԷQ2ض>\W:yF&j ?`ߗkՠNC=~W€&H~,wǠ0s40}>`0:%AceOYȷ.4 ga}u7+gO=_x]Ӛ! ,2:?{SZp)tGk _VYj((x,ի㚚+/1 X{K/@,JR( [64Awir}y/bO ϪJSO1([/G] ɧ]P W2q})#|L9]l y1)Ca(,v#0Pv}xw)c<8e>JFڴiF*ӅS7czd&zu&j;8KM`{2"6CV'"Kd^ d@˴ !{S_8Ho) ]}wOUs&&6{imt޸Ut[Sl3f]'}JLqu 1'B%p0UWzj4gB.zeI˙ d"'2FhAfi y Hccm/= `5޸bI' 'ǁbluslЩu0DO aol` ¡s.S ר\bL!scHr8$Fwg3 z_>/GL r}K _ \~z*>1z v"@a,)EGqȦNR v akblqk7ڦZ:0 >,<[%F#)Јe9Y)s65BMDΰw_b*n47613yWڧi"> "Mڎa]2 ~9D%i+ok_\Mjal9^u'`z:u2]=wVSXn91t)'/Dm)2ϢpUJGVs.BK.#ZTkl3%_W`/PX<}1t=w<>z%qr"jVVvl:N~9Ukwkb]c`: 0@v[g >vO/1q>Z 5WuMT!PK:y +B\T+^MXHy<=ct9ǕHeiLqБ$ŗcE 땅cOX.@E{4N\.J |y_&'j^yIl 6l؈>L:tPjyhVbSI$wNUV6b:oW͒2 "a1.agCe%Ӡ@,\fDb6 K;'&QA\*F l55VxGq]cR]9Ka FOt(NJZB%@e\Q)h!c+gO3s`v4Ky=;;:]-8äv Ex߁S obK8u+Թ>Łj +' Bt0p/:`Bx HƼ}Z?z %#Tt4G;?/Z /6 _?ű}LCg,(ﰏip9+#E//7ϰL>D~FAgHԗ?x !:~ {YtI/b8c ,5;N͓TEy5OݒTcR 2bl5p]cwl] ?-mgjfRjXi)L\Cy*O4Q4tSR>QT,ټ7ؒUQq)eE  HsT֎ aNJo@-ifzOM4{SKC;w((+!ADtp4ˇ>`[I$'+wٜ+WPȀ*52?FGu64q&T߽ce,ƺɖ^6K;釧OΤ[nſ/ !y'zjs{6oҮ5ߤ>{`VD?n. C>&#_6˯P?j)1BEuE6Rk:LժU] rќ vPкIi^ ,w pq )A,{7ށLd/Gl)we\=.>IIvi,wbjY:]˪x.ʯ_ӝIO z'vi^/m̊eǏ~4RLg<0 Z6aF?} #o%4Pߞ}iԲe˜L}BJ -`dRu,tlij1VlBn4%5+L Jz-&n;jQ~טFy;#AcGh @Z y0G h*# !cf*iFs߲3&ՓV_#N;&Ucʭ&MDw{}Wn(a ,px8p+Ul9`s"Dt ΖB8z#{F&hM|l:խUn6^2%6_߯X4zж)T_wq0)z u TryAܗD;ds67Ѫ% /{9> ^Ud@ ͪK1~}e-Y0K4%PE7n܈hIÆ xz˰u#t`f2b;@Mwntv/ m\A±7~xz{ Ґ@N08z[:\e~Q:-}'}lzx&:[]ƮLG_U{詙Ujܖ-հJ(Y]L ?67Sj1(SsA8Ah@ٜO(Z|qkcyvVvER])j W`{h̘7azex;OU?P(BP&43 q_"b7~9?4h )Sc>+yZ`>-_x%PCMQ^6dЕ("b x⌌ ~XFaes1.Mz` ajHɐY '''R g<n%kG6V|&:/nqQ|<|--njC{4}Lޣ[֜zn¯""d3[APT(Y{,M[J?M\A&n];'ҥKettNJOPt$d۹.Fv7 "&!S"wRDW_ p\ zY55-×@^% v"f-4jʭb5V7NԤX_N$9иq腗> k]O57"dN6$W^^!|jZK WN[A]w Ϲ~1ġ8ˎ󗓧(y 'r>kt\A sy!1Ͷ "$رC&dŰFhû(w^ŕ&Pzt4aػt]V}(cXiTQ7iW_/s~ Se:Լe25kQH%&Vjm7enc^渋/JKoe[wPh!,o_U |@| -ZH k׎'E۠vn曜VtљoFWx,KMOY-/nVc e_2|7; С!-"JPMq ׌-kS Z%QjTLJ<6HN{2U5֑ݙ{h־_/H_6餶G!ɭ~q}2#l? eEĨOp˦1(}b5@Ng0 ^:=A C$$_ *7ĥ-?3]6Otpp^n8xb†;-^X+i֮]CkyXxkLZmX7_nue˖ѬYK'L ЀΣΝ;K_Ospke&L˯,^*Civg:jKEp*^8 쒥7F3Ҹ zO__weԡKk\4ӃKջ5ƷO#{Ԧ[%e~aiҖ- #0I<d ImdlhӦ jJmS:?+= &G?/dycm)kvZ?G}4]z1,puJ~,pZ8_p-u⪊ոu !U)q?šk2PU2A8 C2]%[x0pk1TV5EZPXfN֦Ӧ{ik\ y x RgLhLfd%jzm~ l0e{"bLC crma (B Gg?[>N÷%օ,EmgY/X1F~ƟXἂ˯\.s=t '(6=Ʒ ?c=FSNs= y <>G,jx[)i]ԁi2Hv'LqgV/tcǴIPpne^RM1}xt۵hG Cwoݩ[aG_' ,##4V j<ңiDkޣ;O9N4Q@^#n& >%ꫯ /X0wN:t;1,[kV >0FViZ9k@ztzhv%OءV*^kCw^Ϭ\ƃ\T,r|VQ4LD+>-MY9;jv%ˎUx4I<*[a##5ZnD6LUe[_QM]!\J;///"KcY|`1$`1X{כ[6XbT&n߰58702y WP|lY qPLq./નD{a]aF[:ڂ<(e4^}^7q C?[nMwy'իWO6g֬YCWEzժUR3KvuQ^0^ ?e'xB6O,ʋ i ύot;QVjVvCu˚q l4im W;[s獢oL-μEh=zB'XM۶fS\ݦqw]vF^{Ucw)]cV}eTsM(ֳn❗@b'?/5tk@.FP7>,זXjI_cLUv񃗩qJx4S~,^D&/ c.hї,^}7_v4p.%PL D&NbVQ .B¸c:q ’ pygӟ`Oi>.]*G{Dylռys!z z>Yv}9C5|О?.2·#5Y.8|E og˿lx}fku|Xq׫w#yAzMԮy"լҿιxk9)}k,|A?wBւ%tDɋsk.Z\(2֮ Pgi!|K[Ƽz&[Oj,.*'< .E~~_|zKepz S*8&XGʏ{ D1 Bi@Eyɤ̍ϗ]dTk޼y4afZX.آF~H7J|q5i7Ґ!VP9wk27J9:T:uԭ .uiwCA#ޙJI V`|>[w^AX䝗@II koII8%P у.ңpisK0OӸqd260>Ivڕ#X> O6Mvpn֬l}۶mb-+ _wu4l0JHH+sn way7Y~饗]vy@lz-ԍe^jϛpuG]c7@T {ƒN;i٘?ӗkN F5N2+^>^|P]hhؑO󋟥No|?O͠V.V`Ǹã=^[wr9265i$/0@ymYy ~ qI4(  6pP2ʝc>(pA902aFgyX}w~/#?'M%KhΜ97mڔ+Vce:\gyF~ؙplm?>~锘HUVBn[tҎϫVe#C{ ө5V&5+Sn L9QzۨR׿cYLEz^O+Io5`)uCh&z$:D}0aX" @lah4$ޡ֨LMtEnR@Z( ͝RJL=O//R'L8Ea ÷pa|KEηhG0KG9K3p4ebxY=( e˖bƷ1{iݺu96Bykk5[O{Nd yфewnm -\Pza?Vz$Ç5JW5U: 4evڙ|Gx/xX7_he$|Sj^=v$"ٱ-߭D3ݖI<:2Hݱ;gЕ_nSe~Y K-i2٬s٨VpZͩ[1c2 :w7Z qZxokaYteb2xj:3̒ cs={˵ܣ}fp^M7yGӔVa]ڤԠl5n5JM<,iF i MԿ-s2{;m;:whn O:f(M^4jCpB X" $ΖS#S8YRBˆ{iҪm4 rۧPB5\%0Vx z|J_.W25x { D;L@ /LcF?̏懸X:|<ۂ8XG8.\(;;#~GрG^vKodpݺu 1@.y8N=z?4vXe0k8|KyazkÖ˷V?I"ub3Z~i;:߯ʠm2sXq.gC\=\Bb}jQeOύ:e0xÈ[;ӑgy6Fc~^|r {SNM}( 0ϫr٤+WTzcC.x@3> 1e%Ұ$NCx7dlӵ^+;dd8[[[S>F[^F[Z~táui٥m;9i-iP j?t d^B Ɵ(jҠ*eM~?}甑ЍHr>>:=}5ԙQu]ݐ/-vitvtmftQcP 73h8%LWϷK$PZKKi= ΌOmڴIm۶ma 5Gw ۰k.-w0ݢE Pˑ IDATF%NO7p,w23h[t!v{vCu8^ p'pXQ#C,=\NuoB}f]qc?Z?Sސ _A}5I\C֞V\gVV[IAR8A.zj3Ե鴈o|~{m`G/2,_2]/oo, %p0€h848{)t뭷rGyZn]EKVV} 3<6l8L>XSRR| 'PzbY6@>*| s˖-iСkš N懲 %19(P;WUO˾l@r%0(Jx#=T替RͿ )e]ق|Toh-yι]] N`bKÚ+y~Ϙ5oMo[W:% \h^EE/%P^$PZ` 좁Z8(a_\Mmq6Ǣ o޼Y>O>ID0m=Dc(o_+/V. 쎍؅z'u~Y&|O);X}ꩧ@4oh#/?s5%rE=MԌM_l9^u_1PU*ahsQ釞WҐ#ShR>mzivN>:7S˳`Z_!S1l5^oEuJҨMCޕ `s=|Kʚ< .kWķKK1NKQO&ѠR.s-?:$gNbzK6Ǻ{nݺr+ c3~Ivklscڴi4c loׅ^HsΕ;ڵk'm+;},=w~hFfNl5WՁ`V}cC+>khh٢8طz'=4ۻ)>J//M7T=Kn<Z-uB\,v|7`1['󷍻։=hxTw^W%Pv%qٽ6e^D% | =Ba͏2< <ћoyp5k-+B>TV-#ϭ< q&-[.R'NwulUz,@o.ZX ksBBnlS5e˖tRٴkĈ4h aSTWTy(,}7=gfma5O8aYpQ/[X\|]B]܋];:n{))tVQX%_J%bcţyVbKI7T`u?˳2aK-}katgЬYb P\ˈ6z詧DҳgOƱd`c9;Gii/{1Yz ޽Xoj߾,ǶrV'ot⭻ywϛe5ƭSdjZd+f[1KebpB|P`U m.S^oűLq>Iv;3miCYtyTQd;]|瀭ʀW,䗜L-Z6m-#-䢏7p{ Tv7z--[{@\^@U\7A+?G80mt^~ha|O"ua6` jQ[xݪU+XIW5 Zr-mV굩MRՄ*T-*PVf6RvwfӆEhD-ZMGy$ax"y@GaCC9, 읗@q$)}ZxqBY+A_K @qqqO7P8 p9Є㹕+Lz^<+LF ;fzO>4aw_k3o<{! ġa#/XTd,|`4,X\~tʆaW^yEU!C wVG?@b46+nԘc7["-XػbAE0QDo~gw@>ݻmfvwΞsf iкk?n"M%MZMtMnUPo>TrThb^F|Ln=8M*ZLVt$ae'ex=Fl?յs;)H+rbq6_o$oTe "<}tf?|p4|Ci٥lK iiiz})RJ\-Knb/fL% ^o|7qraGH9'l pd=?!NNKI3i~|~3;VL3@2Zx2 E )ٳgˬY[~V?}XM8_8=4et(a`4imUY))ĹրcFcmKǍYtM0iifM믿.\pqV[1Yez/*^&~t$oX7\77 am'߭J〕4f\yHDQI_ lDFAP:/'2i"PTܽU9GٺN|+|ysٰnMp<էueE6zw+T:dSٸkK)[Ӎ*UUu_8Gd,.^,:E>{b2Ξuvi{=.a+wyw?g}֖0v\R,ݻ|6c48>/yYV3@\vL~j@;P6N'#]yi.QOE?,_}O:$;uV'K1+aÆfM )]k̋ x6٦MBYtQ>`Wj~qK[s+Z=fZ7mbMs߾} f]|/mJ3} x 3ˬ5n~s@хQBzЄ)Z_fk|,  y%@p ~Mt~R9bOk [7S:5&EA-qm/ZI#ݘ7K5fRw}?Kuj&ƪUVl 4|U UceK1O~)0NN>9K-ʑve'3@\kʳO O$'WﯿUz8}{+{hfI3LeՀkso4w[Ż|f!2Lk~*@( +:*SXfy" |:=oƩ?!8=43|/Rυ]y 뮻% I9>5îA 4De˥<<ƲS76?~}N=TTg-3b3!sM|U^5@>,x%8P]:= ĵi $ |zsj%V d1iaZw:To|KY$#jd6umJuV?_Ζ߾9E뺱V ;5*aMInic βѶ* ϊj9^j}zn qb̉ΔDF{;0<}M+~7=_wE<:W! 7=~$3/6M\ɴn ,No޼{rM7qq3^,ĕXFi`-?|ЭʔB>PsJ'?a?R>~s3Je_ WҿxalÜ-ux}G?r-1^Bt?ŶYt܍o8(߭\,'regX5?z+ c OWY4(v Hcȑ#m-q%Xܺ p 28Loƀ3bvt%}ʔ)ҩS'x̃O؁q>ti(Xs$x0ez̘1.cAmڒ f.Q#V@X'~G)bӵ):e W_mqp\uJ\WjX"(b_M_Q:}Gq =,1z饗dĈL&Xr Zuf;XD<+XvY_ˑEÛR~daE= dHM)Vc w^޻x˫_G ѣW R]6>袋l㖧z,1=481ͺpUƏNXو5Vch/g5FQo\kZvkXrV^Mup~Kx  בjʯ\򅺾xstRTYS[VUddygyӜ묳d]v1dVz8חa٦>`4S39B3y!P!G}$gqpN15u2՗^ LZEq>Ƌ]>X:on+J$}z'iZӒU\?Z(Lu7N߱| @Qg޹~G +Z IL(i'?-yY{>ZcFb^Gtz: e|9(>3%:`x≰ئZwY#/׉yɁߖ  :iBkxqq]RS;Î"X@줓†YLKy]6i|f~6e-rLU.](>.{GϴnlVImcQ `S[ƶA<[O, l-*96,ְFdz%y[61+Ig;4{ZX۴u^ؼiD+{5rǽsR"9`^naeڳc8GKC(Ԥ{J^R>1*M;6]Y@Cn s>c,|i:!4œ<8g*$g[6`~\@f!Vf. :ezMykѣ5X!dt׹gy++:+T!ݘY)Ucs;Shk { e4+뼜BԟlXvyHa1n3_o|Ic 7`u:sWo;OnQ:帝ZNݢ5{ƶe~qߐ+ٚbjm`Bk~DKɂX3%9h%>rSdV䲝j0} IDATÑno@r%kT4)feXSIǹٟM.MS% Z&ZK`N71K `=6Mm23 qwL+4!]eŦrꈕk+#ϱ;_>#Nˇ,By\={5ֽ,Xvg}l5VRY p:d[M^,呆5kŹ_~"Ji` ʹꭼݴm\>׸fAf&'q-eIن4̵Љ^t5qPuod'+(4$o1@Igkndwm6{C&jIur<3p?vֆ 6_ ~dAV@lG-9ez^;2S9fVwp,:Z6ﶩҦM{怙[74@8&,p遛+wC&:;}3Ba)>֟*g"Zh2 d/+0b@i|~~ `d4;'RbK={:xb?O k^0:Z(f o@`.֫7|o=s= }3WnM|өՖ "\+Q ӉkH*|iV9. %rF9&&uzl4Mf1ց7]]]mVx39L˛vI;zZ:Yw3?oW X:WVap]CKWiN5IB5D 7ZDŪGodwٖٱcs/ gn׀ݛ71(/c}zҁr>ޣt:%4<}OM?**@^qCi Jj ?J/2?jى /i^OyhXkC묄 <.~d?ddٴ];9<_ejRkzH6qwuWB 8C**4i`5k\_S١{8JaEeV)鰤h9Sb5ݢU+`ʁaOX .ɋYLag6b^@1ĀfSj`]3kkOz~c '3v׶ՓKĀqbaˠ[Znm:uô0vޙY^, kpmtLƶYvle4o߲m&!&X 2Y*%w s~P⠝h 5hVYC{<0[g@2E[t+GCoߛ,'v y|dZZ.ù벘ricǘ>5u˕4ɧo!jۥf5~ٶ1`/[Y10~z0tTcCi[kl|&.+UFI9 m߬<6nԣ O>D.9ީiJYXqsk.`fNĤt $h6,/\mzN2P?4(Y&7AN>$yᔞuItr֧4>iHÚ## _q-k=I_zY}/~ص3~e#<WLFLrr_zpVqm^}Hsd g뭷qWX ǽc9qV0S}Y[ۚ>$W_`!@Ϝ93grO_q׫i\M0b0c),w^,iX3~}yMdԱ[3 3dD%^Bol&: oAyz^JA¡KIkQ/X]믿"=/j0-!RY$z<[X}eS,8[h7G9&RletJAuyIz?/%Pf/7YغUmNݬ#hǭ:d47Wwh4f_u9r׬EWh6#gKu]2O7]#q?Þ_4g4P#J{Hi1v6RW\~\_Vz,dWe2hbJ L;xWiZu1N0Ɵɟ4i0 ~ȑ裏ښq 5j0e֯_ߦ3]{=2_m7!7uk.Stu5YɰX[l @V55 L67-<%yI,rF<"쀮R̝ymF XjǶyM@,lO%k,aquk |s.y: P'5š! ^I3i |Cw`\]۰5ɹ=vwd:R7[0Ga-l3&򥋹A9HV?oeY&Wz rK y6SXLVRZltQ *D3aIUVN>e˳BW\>aƔhÆRJTE6ro.+=Oف駟9 fLuh)hH{m8k9L:uo~~7s6Rn=CUݩk&TP`_#Ms}j"䄧IX3 !늱3 5I@@XIҁ.@W_}ekeJMqOey/me岽,d%Ɗ9žZ۶m G '<8-ySO9Ve˖IC3|M;ۘݶِ5ȼ `:u~hߠ Bn}C=xwָamaY/鳅Nj ŸmiNG&Uxq8UgfuZ;l!R 8YD笯@Fʆ+LZ̒\QgK= |y^nR .G7862i9 ͵B=|0A FTDu _~3.\| [5g:37Q DCD8M2?P*ş|dfl |[eҹ%Jf 2 dpȽ\w-J ޽/a'>X2ͬ=mb֕oR^v=.a6+jܺgy1]v8-ӹӓtɇx!z3} x 3]ZQ6c56ko,X*cruOAcpuhCD d6ݴ3jh|GrΝǪd)V`2@V`d iGMC&u)aX uwbg" Ē:lׇtSAtÃTiIٸKKs5k-׀.I+vyK{Rej)T/b4T.k7;_yiq~)]* ^e2 `Ix>Pc=+ZAw2|pwqW dV6Jz뭷7ސO>րbt9 lǏ/wy+xq|LqHK;q>aO'is9ȋeyyFs9SOF[r]v|i^#eyA//FR,Yl/ҰZC{:$^.1c)=0Uej`Fjk 呱j\ˬ`i51B#Clo9 [%| M"ݢn 9i )YCCQa76e ~qҘ@9YAY`<Ҍ˕i|tʲ vEH R$0h/4@t S3ΪbљA w=?YgUj& 23j.&]Z8LǤKi˓VE S潤+_/^CBSNр^Xgq2/JrZU WF[mL+hcd`p<k<cvZhaSGmS53yda<ͪ/г64M>cU&Gy8]OGt~ye<|CK@k8 ӧo2+gip&_|!\s¿< RgT/F XhZc6գ~G`lӇ@Y}ul#pL^먹q9}hIRtn6ue2mПw ɽj\`aS-4 iJq#?C}xN~Ԭ,X |jmN\HH9¤stƙů;^kvpe{,_X$iU1Y 2LJiQaFر;rGbրnҡC7xb 6TcZ// s8rbBb~4/O^!qz`Ĥ;Vڶm;s1p!Y~N;K}N`)~E3MȺgϞ1)r6|sX˙[4йI-g޸ 3K(8`\ٿm)dhs@{|4(,qXZT,#rû,m),wXj&_f)G SFl%audVP%g`9bp&s_=VzDX?[8"ZNn4J,ás:X6boZ`"'o֧?>b/e}hbY81uWnYNcŨx2G,|/CptO~kDVWBiKL5 jvDT< 0>,'δ.=Y /2LԩS)@E!NCt<oW+N%z[ :뭷dJqZn>B4wq>rc-2xe IDATk# )XeK6_c0yL?i2n[;5PG艹Q[l nc׻7hq0A0iɆ9 OXN@ i~Xx'U6`PNnF6El!tnx ,40K¤y^ɥ$w(N^̂ Wy ŸBls_,r\`yYt8`3x6缎gQ|F >\WLY}3 Q4|⮚OrI~bN/ů*_ZS\seg:_Y'/V Z@WYLޓ~ve;c=G _r,nsE]dANq<2c^X7l_oG?ZςK WjVwll?e]rcxJ@&Kf42`N) KkšIT,¤)~~V܂#Po4 F:X=J?>+F[ۭM4'A* 6ڞG}49?KM |r ϲkR0>sM4FOӈ{~y] .^SPRwu҃r:_|}z} ͗V럾e٠gf:82 d(~D9'i aرy¹XX{&Z{ؑzСfd.кsw?]FB@_XOv>'+Nz,@$Sh۶mL72\:n2u}^P1tp}H9FMպufYS ^Rx{Y/ḙm:ZmM2L20̅ ˘ dcZɫ$і{m*h h!d-0XYȫ`7$-p8|+RyN:Bcd/S 2.XЙ%9Ahʹ|HAG=Can-C"?w@`) PU4g_xvc9o@8N9׉<vrK#lM2EFaRO%7k̦b9vX/,Oיtw̓?6^stx' W\Oe@ |@Z||7iYaPrȑ#z3 mJ=XY/ RcIڭ&ϗ)[p7]Σ~N xڂb9sFLVҼn_zEiS`U8uoZx&`kco~z aMX "kXyGk)p\bC<Ĕ@dptdcȉ3lAҶoV>sJBg *H-ᛟE9g:=6'Z H><RSt (h d/ol )L}Rw%{;S|+,*K+\WWXe5y 2L" RR 9@ |A>l[ ZC~y|4N׃B7k1;1Z@1@c \n{{<-_\s<zCg&MMZ* A05^@oL6mQ ۶m[뮻MN9XVͦ]׀/QM[PV ^T7[m 7eztŬ2a"9hu76Æ+':Pw$jG/'fs [8<+ ̈9 j*+'4^iҐB3> /;ZIsClօ# rpHY0X9^u$2K@'vR kp)P˥Z09 '}*+I!e{yii)yY+HR)ڽ-.jP_]2PšoWl*e9@uYL,1e-ӝ9tEX;3f̰5xɋH=ÞWO:O|]X\ԏqoe Ex '_N\n|a_~YM\2x4hd`=mzOdD(I|3=bm ˇ(R"Jh.&OX#E4fr:M lM 7ԣ,~Yԙ+VC^2Z?^n]./'J˾ i`$GG_-?m`Tdz~$\ $d4jV=1gc|\ K> Pw]ͥuYc4i=,v9s=q SX\oy 9@Lѩ4i5x ^ JDd4, ^&H+wl}ab8~gN;֡BdLwqywNq,N1t,л|q<9a3@(]LC~zb,ş|]s@  )8Ψ?myg xmejId2iOKdř+ۺ_ 6C9Mm% x{3,bvUE:dBQא^8+5˳QQY]8{i>\lGi T$z(rmCUl] HӛD>\o@H(QHBSift99M.RO$M^isejׯ# j \=^+O3= 9Ȍ&۟-:*ySRG3{?#hW{[~7G +~/9Ufŭ^4iPŊkY iCg<\'# 5u^ ~W3e 8m۶X'vNz}fYnӦlF<,|p|i4aq^Λ7=y|啕#sҎx`enNjx}pZl" XO塇xrYldk+iU]v^ʏkSZrʉ(Hfw~kndG?Z4#<IXRpnY%Q)}gٿ]VuYvUq*miC}|0^o0֬Z6Zm93Q9\ )Z>MalI9G$x9 6@&XWP`VZ¹%3DTww\`+7WX݇ya|=A2XS/7Vr帟GхpO.ybiIs!AcYv?]߼9II q?-ts\hdCeJݎ((f/$z'} 'zI1˨N ӯtYܾ4-Isz [S%kUCU^Ue2 K =lMwmf1:tLfJlE]Z+c8=;@G3?rb:/;-c΋8t//!+.;<D.ԩeh2̔gsFw}gך Lfj=rYS v{4g_+]됦< ԫQU~M~l۴]2SǽeٽnM$Jh ~ࣇCΕ'<@rOH[ȲsWJ~Nr[q KBlO"`zBƣHG^?B ;d+EKP%7oEM^u[Ӏ RK30i8 rK $?eZB:/^";eb5\Mʨu6uɸa#e ϒ)G ȳzRcO|?껂tn ^٪q^yT-9`,ކ,nv_s$wcE< (izef2 dX4 pܰnf6Jb*֌>_Xr<O؏tq4|ª _{>6N8<\4O' 4MhU'qTr[H裏wߕߦs׎Dz{:cH{go+>.ڵC=4W=ϋs0O >eIr?w(#’g אI19wz`+xg 469`WMn4ڸwn" Y[p)܄ /Fn< / xMSw[ZW<.3+ \&뿹JDG[,$_vlYY:a|m⛌q7 q0E4%ɴJjN}HmL]tI:5%v{"n^x:O$WuTJ yH_~./xGz5ͻJ4<6nNM#)yl\"cA{Pg/j\g]-!7\}0C/е2[5L& 6̟%]PÞ> Z<_Py|# v׮Kq(, !k믿R.Rql,_] +s>zXȣúlj;-)7TzW-Guy]/ <~9/) 7+WAo~ϻ#NV=Oox{[j±I Sn8unm,Wt&G]oO΁Wy~7j-Z/[$=C4В5_1+YWNoWL18r͒WG1ha@ 3] e>(GiC뾧+J:͎۴tP=H2boKW.txLv7k,WjܸM_fqv~We5aFaiqڼ@`ihtx)B^<8kk3 50|Y:}7X?eiuv?#+',dys|x}Od+3C.e5HFL8M(~-,(*|/*< y|˯ 9f_yr!l"X_̓_=[ZN'uZ?^ZVѽ]?УkFQ URP3V.`ظOt 6- mza>2xnE#D4Dߒ''l/[w] Oy~xi9Yk 4rw,__2stur˫J-Gy}̃[-3;Kw 2\v3zR>J:jggz[d%q?$T^$C#cW$e4h鯀c9Ʀ_s5v,N!@V cƌ1 "HS>WiP9it"t f XblrbqOO^FtBr]N:t>XldֺuR`ˁu|pX}ZK .m3_ ?p; ZLDF&vb~b q"T4/k1݌ǂHOpjȌZJ;ɿɧr|{1L _: !3dCى.eKƾ"|Sѿ[_,g^%/.ש[)ŻݰebRzwЫ媧:K-> a |mVQ`nv%6:rkt5Xz۴i#g}mv 0`mE8z^lRY}XEukˢ> WC_ۀ?IHhb FK.iڹgG :T~S.[Q$S5ʀqKM_d22ks/Ieut}=ʗ+Vkג%,z\0B>UJ&Ϲbyu#rc3d/#u>9GhjqY,_,m,Cx,y/庋&hV7) m^,^ssSUБci~%c//Q-Ḫ&G]icϓ:D&B y=k=?4q8G aWCg dX(w rDW)- f>f+'ܫH\ 3U;cq)W"ےU~3d d._٭d>DnOKg] Eq./z/dzWRq[LA$ ,aYS&Y9~ 8\Z֖".,rcm&lRPJqb&+V$c>4?ClS59pӬlee3Li )jzN0A#0[-%KĺV-:exc%"ak";y)'G= gu;^!]wPP[Oxzd,]Ɨ_k?ϧIQe 1&_T= Z6|!?7Z+yޒ:GNuRVES½F0пeZy+2Ѽ;yXA*aqaŏsiyI3?$LN~=5=iw;䅲pViwHQl`ZfO>,>aOןh`xlgj\-=DNG7esdIKeVmp?҇d"`uGrKA"m[@ݞʌ-dJc"{kq Uo{i9MVݒjdSEnq@7x[ܱeW= JCz1㰁] ;&M;(mMiƪ$Uf4k Ӝ|A[Oe842b€*,|ʆc6+=8AdLQ`0r[zqdԫP*Z^~mM9C*C' gXY K%SiȄٿvX̀0C_f4 K.?G7 nn]deCw>KQT@.c7OC!jw?FQ9[UZjJjj"n񲺧/M'.T}_cגJM^H;ɫ^ :G% g}%)H1#+Ob\`0Op,6G;^avA.5sg.Go&GG啫ӵ"' zKCN(^Ɵ/%]%p h nWees5;F^z+r1j-޾=zX}ӧ-l^TA7w,ֿ00wJܴN)HFO~- $kI 11\B$I~*Uy[EQ{LD L,YbӞk/밾 igq9qDGum:A/Lm0G5KkE״U+"/ izˎk`5iݯh ѓs|鱌8<ӧOz?_N>d[OZ۷_ ϗgLi sizslMG qȐj4gZ=T*&J3py͐KpA=Y;VrqKgΑUjJuĺ3Lwuu,˖(?X$z5)+x-<򗪥kRMAuLoK.@wN ʧFt! - Ƀ.,YH@2h /@n(O+uW)ȂIFuH/CʔYt:dPiϓVU[Z4QBUXLmP~:=iܭ4hQ,'Kvfu Y///i/W0?PsަJ JSx1sfN9:czzcPNkK 􎯈[(& KI3EP"QT]^K7W1I+)R\ȪW(1J2i>sx|=t:tӄ*=Ox 1g1NC2ԏ,Z٫* g4k{ҩS'N˚Bv^,Ådb=l0XwXnyBe24_y?3[un /nfwMEx1lLf.^֢E [n;o/-™|eʗe3= )k,mi%t~ʟ EB[wMCPYC K6&:A iZ_V4hK)xlז& 5a|x5 jzRЊ4jaքDо_͒歚k[5 :wO}-:1J$͝OSv &|NK ;4Te%~:EW~ JWv8vjHF::I;_h|ׁ_-t4})?5Jl7'aPIÔ7۝>YM*58Q7|ӎy뭷/"+_JM\H9ұ=zi0S[ᄥnj'7MIݩ n- )UU%l*?t%'zjgCti3'%{G}t`g6y6qU Sj{#PF)O2fєsH8q\~GĖQS 0L1b13ocV?&ѕa-LVnfo(&-4Pŝ~T-ksv&ei,; K}y*bDd1ca5R k6*POKɫ\Ky}VuKO*Oa&LFj3rL? 3 YpJ~&i?7LOh965^!v)f¡h;>/9+ L'c8% uB]<# qĈ.MwaDp_@8˗2d!L=Y% $oYc"13τ_~9$BD?'و08n,$7Ob\nE" &nȇ*9f Ʊ#M fS-0na,u5iuz" qlm=4M&d3a„J"bqd.mtO<&C`J4pU}JCW 7p<Vbvo^s5k%^[I8zOrҥrbˆjhx3m f>?́7cP!&^dC| Z(o6q0YJ4(܉)ň|0ݨ^0"lnKqtSY0fǸzOӭ'݂9@6R wygXk!EL^xᅍ4f`zW[:R˔±E#IAm_#CznPEӼ. Ý24dּVe-φXXHo^xٍ#>ݜIG]w$3&|pamq*_N_.@-LJ`Fa0GA Ib1 (ôksgaxA!'!T3hT6W6J=Osld8? /"OhU0lC;7Lbrqtw(/RڇSwֱ_RH.E;j"`SK)~;S8ª4,ue털`zV@F̺O#F_'뮻)I"O> Zh!#?7i-(6o[5J3+"Yn?m{U:acrNe,Ӳ$0+h3STu"ĒQ*ѯRo~3jy1cSѼ|alDG?F}+ m}6JFS;I$F( VjX<ڭ[-ų\tI_.g~'=S;@ eWᶶ~ݦ|K2hooGl4Y ̑j(׊Wz٪'!좼&Y"#/?mjt>ȇXg:81.Hq$ԦeShbg̢B)QfWh#V&:VC1Fx#^&rY&MKM0M^@OdyԏpY䗋w 3Mw['@Gi= ^wL>h THZ鉶,I%X·S oEl|COwӍ8'͓8@pWZivۅ>֖n6aͦ.ÐF3/[?6yK4LJ2%^4iem)_!C hS:ɮ#x6IA2-]jSwfC^LaG;;6?!ízV #vվۍ\Ύ zjT&֘tIFop衇Zqz~/5.,;+0~4"WJ$L1rYsB?lńߝy8(ǀ gbƴ3͓E?6ysC IDAT4F«Dj,,yWZꆭ0{@E5즉x($|)E ܖ"MfxR$ß@?tk/<ݮ|S-(WܳYݿvkㄸ[y"GB#S[*}饗{7l6W<; Cمip@s=A UVTUSԍlOUiiXJ.v-a?z#ƍ?fQWz~>60|m*8'Օ)C8mE;#e>r| 60 [1el-h&>_SO?ttMl7ʣ76dmq>/ xvGG1C(FvoZJM7)1FZlt֒RoGS(yY&1ߩٍ#0ǜw,9,v7Y?il@_:94&;~gf,}l$r"-r䣩՚jmt7֏zVx4,<Tjmv,p$^m \eb세L .I\FfGFn &, [dR;'Ӥ'=~w$A nl}1;g<*UaiܲKnirnE8蠃8*>vvtM×=o5:!Mb 2DeDHo$F&3kB*Ͷxb򂊂SFmE FYa3o| MMg_ۄ+1\ΦQGC#COC{vP/sNR^!niy'C0t2Co$EÌnf!Lc~l q6~iʑ<6S3 Q^|T UitIXwujY1cy/\a1y4Ah1F 9i0FTvFs rצ xCj3:߇Ó~O]#;Y\KL5d f3ꫯ*oL;IˤmTێ@WM[i??`mnD-p$16mq "'i#2N?%oSF)a ,z/E&c15*7ïqwG]tTX[^TJUcbS-:j*[Ezkkl?DY~/$DBWL1eSztK߸Y]pB\p*(/—W\Ѵ]a"" +4i^{̎ӬOծʋJeOpcdM4D@۾uʡ\v |4]x6 BR}/[ Z^fO>9<Fj]j]4:JcDv{1|qNlF\GqmGbҝN΄s1QvVSw; +/vEL'cBHcE l+8(`$x@Ftc=kF)@gM[cw?ϳ,->"&`c9qFkX GtDȰeǖGqz??szQKiѕGw=| [k*dDOn6U\"tc?XLWa̯*SG!"ؽQČHQZ+\N[8 P D Ⱥb0SR֯~ed0DK.1R̎lŔY*i iOyK4Ne4U7j(hD!nL g:xW @Dў2ŸQBrѺr08y睶iLiTwGT A@9顇2 /i h؋zf¹ϔLdo1&쬳2MmZ6e5sU'F[b\<>ܦGgH(Fm$4-P*^'i!3%HaڳhfL#@G]"9>0lx`.2)V/ɯd$__hla%>?H%t5>WL[Y^evB" =G"]UE'T)]ve3!dLGa]/9[Ӥ2K7MZ6B3 q!KS4NH#ӶbmZ.n/7D L9p*0eF,u2b.|`#0:b9|PZa u 8x 7il*ϴi4kH6q'm_nG$O sj09dkѬ,frI8M3lZR7bTN&PA>%A+q;FSEy#1ވo0i*?V*aE@!~KWLm!3c&.İdF@ _japѝPZ4ۮao6!۽/({@9!n1#h`lSBOES NרW%UP?!zgNvwj+fs/ZOCZ/CHҠFcͨ(C^cȤ8nS^cjOZ'U~{ذa+FsѠvB̔xhwypַL̔hNڶ6Ði4a?(\h%ӨÝǏ N\7nE7oe>Y1g̍tH>!Ҽ8NQhܟ2g/F #BK+4Ʀ4aΧPS6ނe~/Wz3(q_gHw6ONS.' O٨%/v lX h;}{y|w-3C {?7O oY92A?%Re$SbmWLGӰZ J0!bDM5\cbfes~&ԐDq\_>6BkMH5뭑-c4Ca]%J|dCErTOb(p9 f=6GMhȃ*mG`z @k:0ՍBbg5hP _:|fp"cVdD%MS&gc1:['lӪ#U} -g/@x𾇊=awn ʺsIqge@8oeX2| ;1{،ctXa䏦_ . diSfc4 *Bhe(&̟4\D|I7B;`V%'4FM,a~5߻Ş~q)EbOQg̽7EFLܞ uB2(=#G#Ј4/<p3hJC-h/SϟiHW^YdM͋,Hqӣz)m͸^x;6 _Id~3S1ǎ]w].3wt,)M1{oftF^5F[}mk)G_?&|Qc@4Q&\6E9cbp&V+%C,gX $Bp32i~p~wxN ~GYJ7ٗu84nP: pȳ72nZ5wk_`3 q3(#4@JNahhFzkr̙Ĭ9@V帪%m-Z<0N/6b̮W4mmma 6LbC)vw!!P F tb:7L"s&[OUm"S Z4#ÆYf4b>t>㎳uiT'G~)G`z!&ooFIMNX$ӥ'O8@5lzu=\N4cl S<=rӠ <##_p6ᒓ pMlfNu"SkXݷEܮqO4QOaqOQ N[g8 Ur ,uQa3 jQ\J,lܝ'9r8syFA5/f-O~pB#FɋMlv]N"C;388m6ŬKFvwU]2FuKRw8:K.b7i2#_W6ewُ\zƍ#  3IDATo1,~Qa3 |fbj'8%il5b J缷odH94hCv@S,83_;.A//-G C %Ą> :? ;S k&4\nO_T8MG@¯hwN{8=@ %KU@ ?}ӞhJE引ԍ_N\j8%fG}y_4ŜۋѦQi>rΐdvˍ><}aM7 =묳TqW:W9*W]+OHEvzc=6 2ĴŬ%c=l#[A^*7@`y w>:E [aXb6ت|aRc$SIY1L.A0,߇Z{h 4)$#8i#'Gv!/7@.6a,z` G.u+lyzǯK]"G`!ܐbvyK^qƔebx(*n)\S3$+QiX~ẔLYHdt8>o@w'x*oo?G^i%LZn3$Z_xdH 털+h#*BmqthMYk k͆$-EI1 yԞ4]SL4H;SM;^veF~A>h/B͔i`6tw}׎.b9s&Wȹ{h5|eBYmK*9g5Zta4e.O>i!vݍ#03!O>vc6ZKC۳- a@8Lb$vkMp*dShs"Lފ w=&LOX)>qxk+,{a|Gbg+YO)zpa>bU1sb+f)ȕ̀8H.Wp# 0l M# l(V"ӇӒ\smA:thAI[oَK.b 1\9M]tQ> Qn֨϶#OnF֙ o>;I/8&H{M.ϷzWWi6WSzNy|S쐍vW_5-w)FrۆqƙVO7xZ~W{o0s ]3,4eS- lf`faroExmʍ3Oxeg]儸gK9}] q3L/"yPq,nH$ͮ[l1[<! Ic^;ѣGۑH4l0"hg3%K;?ߴېz&LuA6@v՚)v {ĉfCdь//m#sa㲅ZP;=c吆4h^6i;gqA&71s\袋B[[[G4Z˴|,Q?-Wy7u%vrmPXv%’Æ9`5'F.ϣN$9W{A~ӰQP/Zxó^ {w8CݦY`:bbiy@pׇ058G''$1۬a meXumVafAq  :! g(B<=aeSN)<9ǗCU8VY=]y[^mq 7:t =dplˠ ylK|E_~F^|L`}1a=eo$43 7tS7ak0 ?g.߶LH@9@oGd/c0,!am/H9ͬS$3 = c}1ExM9!nU%W>ByBxssyjE͕R@m㻖4H6"hi FrɾV;@hDX7**=a<W]u/vҢ=eZ1/s4X4rK9r66Bn]1-NG2 *.[9 { V38-+uO{4]ZZii3/pb-1fCXɜL젍;4M Τlk= (L~w KGk;*|aɅ"-/66К!wuS 4W+M9*C1UP_i7G{٤kV3 g FuU=ML'f(4w,&L`Q.;DC 1ʯ,9j_Uuo7y4Ӡg 5y^P{l阐ma3b7N8!@k]u+ǫnؒIF}p}wbxk[a -@8g\1̞;.Q0R`] y /xXgu l|rӁ2݁G-V 4.|(Xd~G`.7@+73^.1 cmE8Sܕz8! Z.A.!9] fdjy3@d)roAn^6J y? _~^~N=T"ۧɚv7e32H>^Rz=ݪy1eG> cqh!O=T\øˋ_ 2Nl HK/d Roߓ}n#>>i,cޅE.+bi|@+1ipʔV}(BZ3s1J18 7ijdCx`u/1.nyl`Ŕm6gTFi]+~UW]eS/Bbwi4ݓO>mC Ҽʦ\Fj_#?Y5?.\bcpJ!μ߸xj4rE߫.)RR̘b iʥ:!.#~G! BO+!.FT6uK d)4![laӬ>N"裏ںYa ?y8l)2UuQvTy7&G`e-4ZM*n|`-+hW&^\9@Eg?V.Hq-V0.dDyːGI}T/bM K3,2L|SfPrG@W:dTڕ<֋OR aP b3!Z]vň-kdө<1c"Byxovs \rSUr|+quoS7_dLNFŚ3֣\_vLN˓^dvG<[y&\%M2|0i1mGn~H_1ƖVX2$XH832I m!V+Vee׊kئQX-^}Օy(CtAk4ulu9焳:2ǮW^yiVYw[Ԫ'׬)iYJUG2TTa#8C@[=p@yAS2̳Zk+Swa))xCaīv?'A8=,JVlŗ ]+p܉'8:X|ںXl4hB5K/5bx5ɇvXܦOs1gQ_}t袋ڔh {y*cچSUZʿQlap7aÆyڥYtZb%)RC~%h\j4H.7u"y(OK8#0[~Zƻhr+ͧ9'͠2#P*4NnٍOeSweUZaʃK.d83*Θ."[#̱D˴iHs&r;Uv(v*/W^#Q:哅vԙr%Se7#ʨ4|]U9#=\xw;'Ľx2]yILFˤq'N%VKӧqjTVWUյ*=lkk 'tR&L`k?T[@~#T+/V䔧O^iˉq_6G96'R#HI14ij6[δ,LxW:>Gp_=vm.316♴y^mGh1!n1#03"6"W1s 6+bgfzGp8X3;kuֱ#sAyǥb} xG>NG9@?@`ԩaᣏ> ¿3 'N43<n6CewmQXk@#8?8;#GmƁM;oo襹;5\a94̇_i{Q*#0pB<ވŦLX@&MTw}1>^x!<vF1ٿ;urGo# VX!|K_  2;< !fW:>s!k!@?ACr3h"2l饗/gIC|z輙#83t>K|K#LaMv"rn8u'MuOiG Ko##L d4x41Ða.iIjSspG*"z_aVZ_[杅k];@C q߻"Gh \h 2L H0XeɰtpG@h1z_ZbM{JvJSB܊x#0#x濇G2OphtF֧KGp$ *ܼpcW]|E:p'G 즤7  fqGh1zG'*Qkqu<;GL|Dǃ4ESLFrg 6p@kmm}Яq_u>d {zGy>^ `H\t80f_Ɨap/=V3Bf zzx00X˟  3{Q ýhx؞臅'DG#g%%''#+kM{X#UM(K?2A%KX􊉌K 3` P:6V87 ap A1.h '`L0*X_.AX!v!9H҂ !sr< ?(Cǡ (**&tC3hZ>C;$`Bp#i BaGD!",DqъFw7Cp!D5mZ" 7mm m+:j:!:]:7D&a5zjz2>=(} m)M",CC&5 odFCFo3.D>ыxxĄaf2a d`4ĴȬ\|yBf1a f92ɲͪzuuM͇-mmݐ==,CÆ#C5N&NuN/tF\.1.[C\6yùs^a dzK }̧WoEG!UH)8_<uA^A ÂυT zd'I-[a6aDZ"mJQ 21Xذ8B\I<@L|T-**Q)1%ԕb2J%AZPE:GWLeYFYSvrbr^r%rO FIm|.(L+-O*(PRVTSZQTP.URaRVTSE&Q֨Q]R=H[ a  9->-r9m6ER{AG@[JgYWT7P=H-}5#]Hct!CFCb×FF~FFƊƇ7emeRcnlzάl\<ҼaajqⅥe-+`ebujZ:ºccmSbVmW{=3"1=t5[NNNsę9Ry@%WE4Ƀ>vp vNNqo@{8y\NTR6=M=};"}~%W,-,z-z}›e嚷ro;ni5|uw-="?|X߿) / _z67_~ ;}ʷݸE?D4b/do/I@g/ 8@G x o0vUD %Ƣ?aVSTq[x4LkFM_0EabNdg]fpr y|HYcaZ:Qx&^b^r\tel# :JbD*Kj5lt$tyX Q?5362hqDz5kv7n:695ovnvi:z`[{Gs/{4,A!ڡ&a>qg;cc?S'%*1}g{;W6{Yr2 [XUk׳om9mzv^6]=N.n{T7xje<87j>f7eum}s ek>|b7ogW޿Qj]0{_7˿rVn3? {qLQpߋ)W/TO`A'A$"&**&,N2˲r} UJǕ#T) hjlk.jhw4^+/172:i/$RJZFԖl'h@rux=}ce'łBCUôÍ#l"ݣ£crb+nw% 'Z=}KN,ԞeOǤS2,2Ude8>{!EgӅES' _8T>Z1Y9 g+;ըk}4pj0ghHhXx‰ɌG=1ͼӂb딥7W;̽zAܺ'FF_7o)m~!}gAI_ɯkpp8 y `kw B(@ a.ā2\w`l@8APUC%!0k"[C#!ȷ(ARzk ӄz`;ب⩞SkSWuQ{wQAp ]*=$#7c QĴ̜BRΪ:̎cYe̵]c[@BZHlDp{& mIT,+Vl`Es%9e*:ISZK[N'P^~XlIiYyŴ5YUؗS\,dq3ry{f-GFVGbb_' G}!:YS~fg_.辮\ƭĬ.}pY۰vyq{?w?pD!t qZs4/$i@ǘbJ02Qqjf>Lӈ`X#Қ>ӣף`0HaH97~;Z'Bd3JQU1بxDVtLg~My7_=`:__m#gooro%x@W_zrɈ}5$lAB"x pHYs   IDATx `U Q j*m]ZjXmiVK_ۿmōnXZ Q *, H ?3w޻%dyї;e̙!̝,IT P*@@#Sy#CT P*@L.o*@T PFAQ^V P*@T{ P*@TQ*@mT P*@ T P*@h te堨T P*@T P*@FyY9(*@T P.*@T PFAQ^V P*@T{ P*@TQ*@mT P*@ T P*@h te堨T P*@T P*@rTT(P,^{KH:o_ qtiUfX BrYEyg>mٯ ( {v]>H;E4Wtq(Pn{Kh&/*[GvHAKl DbrҠN)zX i*{՛jefJ*3E )5i%{?9[7~ Wvk=DK<%O2_ T{l}_ ߐ j5a_KS=mL6;otj`AYLQ~ ߧ;Jy=Ħ@+@M+Ԩ++%]ʤFʔCIVqfVU,T ]nS(#9DGJv~Q@g 2(8JF ,#9*@@})Jz.h` pyv'Erh섨PV&=i{)ݐ<Tj E4Ut,Y8G^|?u"ݺlyOs{a'O9EN>Xr+CK'-{`5|yeɲ@:!g~l9ujFe'?+s4I^lʗ>_.@N?_8%oV a|RY;`{ l^D֫v7K$K:"}.*Wr_y߯,-"G8x$}ygs Vӫ!GkWio~֒>?L'٧'"f: yyT/C^~}X`[_I/:_oo{8Wg5X5 Xm=l 2%QI99/I(w}u2(=;Tw#!uyJ{=9GKЫ偻Kr5O6Dh! RM#*PX^ ԋ%#D|OLYG%&E|U"f,%{gdJ^K#eE"> #K2+fǏ?\?|\0!hSIR";EöyS"E-]n}Eu8Ieܪj eEŮwq޸IIǻfzź3T+5}ME?^<#R꿲NP}gdZAq#9x{"+*J_mSy-y{ 2cEQdŴص=VMYL]OR]߰w-G|yz/ʴS@fr*PktLʏ`nM[d( #`YUS"'iYgOSoZe!`I6eq.%ٹ"@$i?y~SHd"o]ᗜ~1W̜P79J`ZE>^3{Rʹ:Tv Vg˸Ӓߣ 6qKRKL*P &T DWf7;N8|d+F͟6yB4ͩM ?`_0~Bdԩ) 'II(l$iߓΈ_:b48s}B9QÃEc[3eBd|aad\jqI",e#tXML132sP/j0ޢ(j|(25eE{I~(Z4}"y SEfϟ3sZd|{!W}+c1mrEd޿pjks'N"I_bݣGBWêuIoMwP~tk\R~T}aYJjAFrѸF $@lޢE("/M$h:%*F,+O_(J*3oҜI6E&%L{?=yw x܈5C+VLKzVh_(/L ?”M+Fa'^fM) ٛU$ |K65ALMc)7nJdEcr8OS{>J].qe0^$hUTpHIwn¸+\r~򲲐 Pn y HhZ}l7?0xؾ$![-OuH5cF^E48tF8aZ!p` t#5IhӒ s8"q'xNB 8>\ADfOPY gV[4?qH5`Uf8#a)t7H &gچ_ [*{cT&{I80J& ڧ+O<̟\<')Sh)/IXV"3ew];-6^7]9o`B}w[\^[eXnʗ- ϓ76ri?r' =)@n߿.{ⵛ?h SJS#vWeiˇ%[}J%@* BEQo d:i%=W"o-og4eʷ˳?^3ك+%"2(̥a7ar]S.iw'ˋBɭW '?riqϿ1Nr_ϟ21Nq+.03\5_.<-"=O^fϘ)3ggl|P%M/Uu7]0|X|òEV kS(gO1VSI{+./}dH%@tuf@L tMI_2kۭWv dde2 4i ̭NêX,zn7c+VgXZ&۶5^*/<7K.[X$wC:a. W2{2{t{Ox4:/f5]wjiN.&7BzDG 3{$MutMgnH_B~ H~ 9%5PAn䶇S:4o,-8yX5owj+tqUr_fdȘI ī?i:hyu O//AT]f]|_@k~a'D'ja utIgnHt %=LfR:)@Э|\#jOIKd^(/aau-]8nM:vՋW)p}%[e)$)kۮ}\U~Yu3TЭz(H7PzHRkwk&OzT'ZRj+@ЭT4L u 7/_ #~}H~QrgznCwUY]e˯(ڪVZ̥Lj޽{о^uI#3o.~i\98t[u/X/BxeBgi߿I_{xώJ ~0}Pj~vG4W[}h ۽q:}F/&/Cie{?;.[NeXRNoY7#j eEO w\s6qruWRQ9 j=2n_n8+p:Wv") ث&qZ߿"j+N{2Tw+JP/򂕨@ pՅ*aaz)P*dKSM.raXgGgyy . U8HtʷcYxRPzNf?/f=/+'|L̋C=sC0!=y n}}ҫK?{ɃRoU}dS<'o>({$/)kz߻ZjT A7$_9a''P*SZ099}֫k&̺7޿qDy\,O?1f[G7:n8hxUh;vVc^.s'^0ePf= w&&zQB;X0UrU_Hu>kr߻uV5%E~*R@|.wruv_+MK81>ԔYz+c=m_[VVM0^-<$]rl ?]+_'|\e7zy8' _VؔE=ϛ$ZnEU_7͙CܽQM^l8sjK$8w6'*zp/. AC]VU+ePEUרV6'92{Ndr~ȇ&? ;#Sj+'Ța7 >O/l%m˜'LLM S<$ԟWa#j_HbWfW 5Qu "xQ__q/BE%;hxEhujW/DaFƇd_O5ŵw6'*\>FT/^ VN]K嶤WjP5֧dIU:s4)7-մ B3 :?,W+Xg15s0d|SfK[_B[y$3'ϝo-_&낶sA+Men|qR.䡲M2cRS`I?y|\vg#Cԋe˜I- HLKB"]F^)eɤx8|\J277G_jfOe¸y(W /O#T1u-UZn+q)?/K2-@ 4!WmR* o^7ouf %s|ZYmR2s:!*%˸u;) ^y^+x,ڧw3tQJV{Ȯ}t8m!!kkl^Fv꺱QvCޙ)o.\0Orձfx aCHXN~a,~coi#} #kz WLGR*?)`~wvnGorr`|*ׂN{ďnr.. T PԆ-;EH2%a#m(tTu<{ñ!6jx4ǩ *r T P)W '?G!E''Y/6tu()Qcڹ'$q8oP)u]䣻yدpA_C P*2: =%!7JfC͛dks;*m\Vq_9e^tUK^]T P*` lx}JL*MT)z[ /<9h-4x4؈n#T6 s=% t_8]>&V.P#//p# }*@@)P]͟#}̛T-]j]-Ǟ)_:$1Q›  EKT P*@uWS![T P*@HCixQT P*@@ ]C@T P* t%*@T P+@Эl P*@T  EKT P*@uW[w T P*@@*@MËBT P*@A*@T P4T.Q*@T ]n5d T P*@iA7 / ]T P*@ݺkT P*@PڧH$3 P*@T h֬YqpR{18pP AT P*?c@͛V~ + z}T P*@ Wnx^בt=^=߿ߎ}χ-<u0OT P*+xxcoѢ  rY_p<䓲tRYbEg;T P*@ @A$//Oƍ''pddd=l|J6S0Mt {_Gl?ڵKƏ/FÇ4T P*@ŋ˂ 6mO~9餓 x>[ tÐ(`eeeo>6m{\ꪺɺT P*@Hp G?\r%iDu=v 䆣rx Ojzو. BT P* >ׯu]r5HVVnnm]Ds#{sʭ*֭Bnm. P*@T+зo_Y|=Z/v \|ǖQMk r#&Mg}/4T P*@hb :b&ݻm+fi>ZYjCg~̙3[nRPPPX P*@T)pyI׮]e֬Y8W"HR+EG\<|VZZ*/\z饵uT P*@Gp$x\ 郬5֠ss=rgS*@T 4aƌ#oEt16qBmz膧.O>Dw^_X P*@T*[nN].6Ơ>.>%%%[_X P*@T*~G .=qeT P*@U< aO`ϚZ.:U8DT P*@j> IDATZ6k8 ǘT P*@@m\TE;8M=z k P*@T 4m-ð5QƠv&Ӗ P*@T H[ϛ4tACajT P*@HdKϝQVT' P*@T*fKƬm3o{*@T PgJ)ݺ8T P*@T tS(ۣT P*@H iqT P*@@b>,ꁱ=*@T P@2Y tkmT P*@AP}R*@T ԻzP*@T | t?'T P*@@+@ЭwT P*@AP}R*@T ԻzP*@T | t?'T P*@@+@ЭwT P*@AP}R*@T ԻzP*@T | t?'T P*@@+@ЭwT P*@@)T P*(++ŋիeUfٹc޵G-{JfVɖ֭%mׯ 4T";Nڶm%䘩@ H.S*@ ysŗ-K^SzM: h'mHS:%-[g/+-}dߞ2)OѺOe]~2`}i-cƌ;FK*D 6 aR*@g_y|?Qh#O&\CN}*i%-D]rGͤdhD7G՝J!-++")/=\+sΒ_qqmT)+@mWcT P)o>y_Lzt>cl}g-U7@YOt%ho!#ꧏ5s.Y(\U$wBܹ{*$;IAS*@;w>(/%#qEL=F"X[(Z5w}w@d\uΕ.]yl߸C^?eʡW\!n UhB pՅ&t9T*@H{L"3^._Yrʀ}%yd4Tp[|e6ϴ.zv$G*-wz;uQireu% kr ߖ}[Gϔ~Gri\Lt  H>bLHjvAf|=?Aoץ>aͣş̖iNo|+_ c*Ш`DQ_^ P*@R c H:˕](ef5o2ܢHo>L3_$\xyOR)ۢi#iwIT >~Ft.nosY}LK h U]^i0FjVShd7Zɕfz_A`dxeCȣW?"7}(?4oW wHՍbr(T P*Z,_wLoʈEM} T"-lL_>Z ؠyh/{6M va(?K!q3 dyr 3QƦA]Q P*@RLy^Iׁ]l`sq*=b+`$)܃ڄ6o<+jeX|Q׏ǦHX;/γsZɗ-ߗU P 4wdc @T E 잟ܶ[۩(6(U@D@ Dnp;$_Y2ԵIIx dT|0ϨƦ015E`w@K7se37'= <7󟒑A}߶]Km߮tY|gWeczcLTQ(@mT PT(sYWV8W:"&5 #װ7KjêYa8Ԙ]P+-/W}oWvhAm.jfVfKwgw_[n%r *+M|P*@@:(ȸ+K+0'W aFÐ"_A]߆S( !Ŧ2eol4sBOiT(p]Lwi ѩF3)1]ugç?"_<oUD͗:&a\7CtנYr Z>aswˮ_K=hp t%T P**rI=j\_-EdT.` jᦃNȭCJZ'joրY/A9X{h+{o0nS>n~zLd裏hp t%T P**~ɮ;Kڼ\PCD7uQP{̠UϢ˄ HO*ǯAkxp)-}_Hcػϸ|X8t >2b(ҿ Rxޜ{*Р 6EgT U رC9QH'.: nuӪSyƠUmq֗yrpMպ<6EXhCe(t\wVC/Wۭ0/s?` Jn\t P*@RLCւT5QRO jD6(7hAчpMB?Exģ[Y0A&m5P6?NwJ;_ o/h tET P*ZJKK㿓gQL~:"a(1 B$v J-%.A61E=s) >? Ц)Fp4h֏|Ժ~0Dz?m=y.W#o 4 BM*@HxNxmb>*9i=@͉u@ ~ڂwu\~m, @I,7F^cn}=g@<պd\ Fc cERiٲ.7vLNW-h tȅT P*8 n9 KQ O;t".: [Q-sPQS-s#Q~0AOU꣮px{A;aa&\hv-F#>LΝ+֭sRA\$HS`ѢE"Y}tw< '\tG2~nآܦ檱Ah6U7${M]Vk2&_8{୬FᏳmn@o9#L; 7dgq0G7H/ ݴDt P*@RcpF`q\Aڛ;i04xtAJ Y. B;Gj@uMx`M=-|Zj !V Kj [[VҫbX84C%&*  6D'T  u_d؅ Ц.yXd*" @E;a1qu3g] Cd5+C](_֟Ai⹛ȵ ={؈!FD)oe˖! ݴDt P*@Rv:i(ggh1\!S \mK u /1D-]tL@z+@MCT <9A?n$PDh/ڞ9PƏ]7G#WGGl)>?f5PEvHt<ч봮k#En&=ҿ}&T  sT P*Jlж+yza>V/) XE`Q_0Z_= Z~cח ~ca:\0jgxy債@+@M+DT % ̖L| Φ%u9r @ EFATEPlQ ;]&qq}ژ/P:u><sx 9Rc ׍3afkPɔGW_}-2QV֗Q*@@x7zJV̠ID1 낽"qn/ ?= Ur5`V@_S̗EB)"/q @zK8\#>bs^p}{D9~ѷ~22d+ n@:+g P*@@x!b:dg_*=:P ʵf,~ 0ҧo{$#sdlGnmtjY# & ݵ}kLY"6AmFmƗsc]E-/ќ}~m7Gx@Un^E)U`Ҧ6l,g8 2Ze5+,]'GO 9\'E ;5@?|(m4OlY9YR\\HGxUT \=HVQ`ޒ1Z$V"uXhRWfje"#''q4٬"]򟿿 ;hݿ[^5CY&O^ӎ8_?*}{ ]t?AٮYuL0cdBæ]ʫ;O珖 { &9sL8,_/V S-`YDYk.-HSizaT V=H_ZLC>i<"͍ A!YfvQu2_ѡ;G޲Od{0cmVj򙩲L~,OB6JL-:h;H^(t]ePF}NBȆVJF/r|By- ^V;7mcT P*@R@Ri&+@:Mغ:VoQl uEJ?2)w걚&|F>u.џUξѝүٝMl+3wmA7D{ıqs+3HѶ-AMZcճ)`D7/ݣT P(ݺ Z spDp@/[{\R!#9f/5xk|O`Cٛɐ)QYdg- fu8Z/ۻY$ײyپC'ۺJa٨a Kޟ^V9ck2^}|>6\ք 5f}5竕U.8ٻ{i8gA7} =T P*:G) q[=[ t&`^c,S'%)4V G4Q*@9mrL%lzf_) r//06Nf]k/Y (?<@~://X('2ߕQ_>Kz}G N=٬2qхߒy28k::9帶wjىr0,f>>J~2ɬjw= ׌h'óbJ%p0~|q-يԁv2(]*@MkCϨMN$+~&Etfj7a$h|]5vHr^$J[HNvҥCPi_\pߵR,Ku+ztӾ?]>fl׀lnΒ^ۛ= yfRW;̌ ͝LVo+vmZKkMpޠ|a%I7 mP_V(\gZh۶- 6 P*Фxyn9u: t>-S:?GTc8g=DfQNjFi,"!6-ÈQr`1^0i`MYlArSU2Hiݯm"Ԭ,=# E 2K8v 3ˈXc6xn$zYO[.T PtU训̵%[s /mL:G :B}D3v h47(n@iAaжY{nC6 8 IDATL;/VeU_~6QEѤFZfsm@h v}Z9qgcgyARmC9SUQ*@Zȸ!.$7;Gfjym>h\1]"zR`ֱRAa]9LH1UHAX5~ꁶ\ledwC+冹e{k%a&VxJ!}t[=б"ȍ>R:mcn?-ʬ|RÙӟX]77ڛ6Ԉul(:r5UxuU1=}r&* pn_!GhB #m{N9 V9S}6nc|_Ftϖ$goSO5bpf{ -5~ň.+/>`* FI-F4A#^<I^cK.]6.XA0 "Ģ Dojm3km}ߦyؖˇ Ȩ_B/LT `D7/T 4=nvnliתFw;ˏN/]tO~zVٹׁ_UjjJ3L>xC_`i$h"ZMKMYBDlўAn]Ef"A7hib|jsNK\r@5:tH,M{u-$'p 48t P*7Et?--3:f@8Esѣ\>ra]ϫwIχWowV)!Y EtA|8C`E9,-_UC$C~H.&ˏ|ylJh>bvІ=a6 Jl6̛iLVMk!dvZurHw?_D]N]H+DMLܖ-AdѦr! nZ9v ׷]+dIrYyurD,r@]y 4<|X$v?@7F7-0#;t\aDkGe"ZT'0)E~nĚ 3w~o dS0 uIhWQ,c iUbEi {T PJ{} T! 8\z|/u 9GȾҫ]kuQ{^XtnOGlvX#h'q -z@r`X ~yAPpj{EkS@S >]Ntoe0U@cZ8uڎcXzS&˞Z%W| 97T  T P&@t|GzڹW#B\=u劜ץ\S.8PG\߾eV49kkejPlt 9: R?%>u0&_|t#agYAdWkAKϭ` }m4uJKKe`p  6D'K䡷픝!vo+w &X iϔEg{%>@q&ÙDзvuVΐ~0Hw5Q qsDdq4 ]_8訶 [F<3"`*Ѧ&nf@() 907A"GR-_mV]p<`<l ?\*9bȑr1{&*` 6KEG G7ҹErL6#CfY:P)M)V74mRXs 8A) <eX" u>٢ovInr)O= AP=Xd5PtR'\kv:?+WP6p jI= +@yL=#s>٢aA][,ԍ5.7W~_y/n@CRې}T X}P 't *)G1,RbT eɵ?rucmDm d\AZ_HڵzLnr慖+hbPhskҀsmSBchؗ{xEՠ{E0x.]J"9E- n WF49餓̆*А 6E_i\_=kkppW靋-*xLݜZUXp[g [7:h֕]tھh=Y,V"O7zWp 8PtΦ*h.-+tg:eDT,7wׁgh1(Mtp| :(&ܺ k^] UEH^lzcB;/O,U˟vRA]0KHƱGor„iTJaEhiS$+,2CS(0<#ky"ޓT3,DNZze|>|7S܋ϑ!gֶr-j C*xcL Gc-:Y"N(on,{{mw{* 6+FVNxfN9GN? m2(5 EQែ'C' L5WM X۲rdz hvX gBO2E9ݶG74edd33a 'b./POAV#ڦlm*'1(M>rVH|E+ ic`@..];v3[ :0QA^;zN+von{h6WnWrZ0P<g1 L=76`P =:[ V :7?9ʏȎ}!ɪ6}M^3NTtEG0֯SqЯ>nf0q"b=)Q ZFlnuo˱Gu[D} õ֒ݥׯSj9s}SA^::NHo~\bo-z)GF(!z8)܃d8Dl1kب<9j2+ZVfVǵCn/1ڇ!{#;Hv[R mU>l'}In4b!庚!m8tg40ʚ;ɅI0\끋:E0آ>5@P}!'/˘< Xnxt P*PW?Tו>].#t#HL}K1: )$ zn:f56Td40HM8w}ۊZn*6sj5H#W_u|䁯VݶsxH YMM@Thj4A;KO>%UWjmZY^  шb|1[-^,#67F^p P&_tspKFp9*@@Mxgk- 6}ŧ2P#z;X3QiJH%@oLN+Йi A~u\Y4s[1h~QkH=rb߮% j~0RV^&wy|kcqEB#t߃)N,QWA©>+N =Z>!> }u$|QPkyzvɽE9?^LT+@mWS*@@8j=\}>V܂<;[,uHڐ,W RsЁnck-S*O]=[$O !15*rr0T P ,+M{SncK"zh nmn.< VistݲCv ?4K՛ǜ{fQUQ*@j>.[;BWN }C+C;{kA%-5+,!lg61CE{k,|G}ǻ]R5>hc>- +~tM]y|GʉCj\w!T[v,z &zu>2,@<1c`Lpv~"7͓ '4[n@cT*D@U`}HyS}mK9V_{5ZW<:=ZV VOpaȋYԃP}m<5>{M3@=ʹq(oz{">r-op}+bLv+G|I1t[4vXYd\xW!N=Cڴh'66[FT/bߙ75(z[@Ya{]]0 szUV}OMrOFyY9(*@ ~+'q{{1SNfT7(` P=8 {<`![]ظ3`j쀺o{dγ]+'=xZO{HiPt}$t0h=vF2KU~|Dy 9r%C%gp Yb`VQuٺu`@Uރ+j ]b eh#eŲC"@)@M BwUFoltmmuh}dGl[]*tNs͢*Y VX6ba<9g- (y4e}O҈W|+*#sc9g@|_d[ItĔ mU{{[h!?39߽V~9g'Jctn0]!O- JpD |>Ճ,ѯaax֝2 E[_'_~ywT+@mט#T)ϭ+ַkz^̽^E.tdA2)$ AT.`T ]LCs]yE>m)^[>ݰK/@  z\w]5F`t  P&V?]\VdЊ[*b Kt:4n}a v q.7 ~Xߚrvmefcd,w:Wv.Y_U=ep2b1Dr7,:}!6v)'?ɒ%KdժUj*yGJbU[JvOlRrdKNiF'G .f][}.`6euޖJ߶2NUFꢷ͏$7233+6D@+@ЭT..>~~AW Q(j;ֽEfc-+UO]\`6H~+_W\}[3ܢk~;Ԅwɗo.2&*@As PƯhvZ[,GtΑ1;! $ݟ"qxFATehi",+)9| baQ]3*֝6eP q" =#`"Oq|^B&='CnI.^Zg3Q*@A:*ц P*PCv/v٩[DoomFopm1}OUpPpTzryhs27˶_+-o]6T 걲 A*G_VS=`T46l,g ж2%5A7ݮTA+(n)C:ȩ:ʡs@:()GU0KW>Zu(9ꂯJYKTt}WJZ wpZ@\-km:FΎ9)? +?{Uq]ewYz^E@ R,Xa%5M4&1ލ{7bBG.;gL^{2ߙ;ssϝ+[Ia=~Hm8H@7+JHHNXU.zu5kӶO PkU[{ R븿Q`_>8l6Ɯ{*L> oЗg8: ]{3{ܐfljLm(nUc?>k 7_1]zK<6 $1|KIIR,5ez6+°͚Zl2.6^P&i*'WQ};"e> [CU2Lh\8W9/'wCᒧpI]oC% nh@tH[YwE|o /zVTsZn5XQ~NrڶmW' $ $ ; $邏ʒ!rZo/vX9+0m)Nٺo(@M8 SPxaWAW^f,0:lmWNE.;0i. fU6e?kyzcܲfءC]Sc@@@ݐ@u{LW$$=J`ڒJ\ba)ڗBݯ7 2 r *rR`K]+ln?vwx{0fzS aᾃr?`pm=vm1J2VnP[o\kڧr;U~к8R:$ $ I N~颒6TUtN^TmJqЬaY9#Abze^5z f|uD]Qc+ÇѼNS y: V 8]8#E^F`]~ 6*՗l TʝhA[opGlVi]:$D$$QJ ݍkKNH$0k\C_'/A>5 {ltZF6o ȬYft7^8<C; v*i&9zqx6݋0j6MA'{[+niRrOРEM5n bK<wtSYKJZoWC{ӿ۲#ڕ$$$H MNW$$$PM|^;i~9,+őڡEnmMtr~gg^gq!(.ԫ+m~W¤ΘĥiK:ǎͧނ/z܋';bOvEo>GA( vk?PdZ*XT^M[؁mӶWc@{ IIItIk_k?\b; lEZsAJ[&CbYo5hܱIqwwkMh5X`rV|`0\| 1h1jgW&G,3mn BB4 j˦x`Xꦐ$$$iK MOW$P% ,Zok_lV 2~f@BTT&+L ~<ykag_1{^hwo{мp4ݼ^E^%S(n4:X&UjԖ&iTT l@ͨ؋o/O % $ $ pNBHHHrep1oA}=gx+ԗVU@VP Qd~lۂ1[F^(acٿsm0w|_Pܑf8 ؊r򖾍}{k{ kڱqsMD+rդq[U8o,k7Yo)I HRF@*g>[io-{q* h]CAn D ̱f`™hV)'4ءpmȮ&=?< '/=tnp~{1DŐZ7|ݒ+ x밬|k\ֻY!.ٶ% s$$$$/%KLHؘ$`u5{{(w (+B&i>B"Qpx},U a)@W&#a'|-E^I5W;>)δ_þiG};u~Eܼk;"M!I I I II o'D$$P %\>5{%wV\m(낐Yhyt-=]^ i^l TQ6ە;/r>/h@(+?x&-'] Z8POm:GK3 R;']&$$$$o'tU@@-bnuGKp{R<{kv2mڍ@,;D`)`։I' 2I@O̧`^1؞uy:mW YhElaoӘ^-l1 ӷpmѧECIIIIM ~7ZI$ŋcժUXbV\rا71Ĉ_|>6sl=B2AGgq9R%@[|L[WO(|#pz^[\{/ZniV?uέ"0Vqkd08}psy4*HlG ˗/!EEE5BkEf6 aL%,nӖe+7^ǔcڴ>m& Ѱ!_LrEo%[%Vfw=zW1bzk4h>QLh]]CږLZor넌YNt+b` 5jEo 5TzyV(٭ ЎU".8҉h d+:|r{;s"JK`͚5x뭷/)1e̜>Ѱ! 64,)D!\]5֬ZJנ]zh}ͻmcCYYYX$\ 2L6a Ii{x0cl  na# [kױh,c&)ALV`Ř=k1f|wpO w!J+C_o}ӗwFӥ6-r\I CFXZy-`ynVP&Q},B.h6JG^ŴxZ{RCɌvUZ| 㤁qX&(g)] L2q;yi{h۳5k}^]0@4g 2~ruXA[-f-śs^CEnpُcȶ4gs^;YUUJZV۱t)'E8lؤ$Z&Mwކ;Mb}{aЗ/"h 3<DsHC.ՃjB!%"ms!]*1{cUslV͛g;nƗ_}\: oCڡai5/kccEM5v1HUYU/?>y`&*Wp!aРAuU4^siڴiƵO)zSn8зo@qF~]+pϔe٬6Ff22URȀSYqx# 7nC=F믘[q+Eupṙqgĥ,,ɵVA^$4oz 0c@-麐VZEnJ@>租q*zzv8]Q^bgUuBThzD!*tn *hB pQuhP4W^zmgyN; a W Ɨ[vN7} k)[w@B_~{kYp-0 %BR Fp RSi<Ţ wʤzU`-iZUeMvN%cl{Rг*opɥ`Aq+A% 㼶s0xg}ܩֈHY?X?[?5c 'ڭ5n\W@_t}ݝq _{ӻy;vM8~P{4i( ۜuM3<䒠ά7`mr<',Ϯyey)VdoֆzV%&v^}L?6 Zm;ZqsISxÉa`v. vkҏbe!˧ħˠHf[tcO[6OV,\:'tW.hC%`ˎr` W_{P,ĘzEb(_ Gr"RR^ZX|-*9aHS k:79^r,:Uh, IDATQ'k%;K6D^&?VCTnCeՋ+޲޻ڗ&m&镽{'a퍢"d|q "d=sW>XÄ0VxGkˍ0|{-M@WLB̙n ^ʷ߾C?BiiK$:"t.c%l2t |LZV߽wYGԲ32=PY.cXg=iuY: .W0zYik6B}G&EVПCZw`O[?>(f)l~ᄑQԥ>y7fU@`s {7E~KU@6[OК 9!Γ8Q&*|$37˖s;- ȝ { eeeYJ%F7 hˡ`ٵoKcj\ +&ZI#`\Rne; ^ueJ{:ʜC?f4Z}i'4֦Ж}ucf&ǂ[WTZZo˸ؠ6q-zvR1.|"Hc]Cxr<$dqJ9G -__ j0{_;ci%o˱&4Ž{le HB!A^Μ6>7vZ5 kFY,W45Deh2u DFk?~`SPCu #//b ٥P'$;ujE$ | X|rڷ;N:} , QA)O b VeA>tN*DC+*6"*{эL!f* ?\wC܄SW / [.aڠuB4@Ij!Tؗpaҕ"lzJTXR=*D>"]O|eŕR?E6X{~ \G7p7.<\tř_Uz{0>Ჳm}ɚMw&)b2F$xU?Xc ѫ1Ue.=^ҁ(&@]=6s" ? = $8/DBsWf9ǞkݕW!z*Zs"d Bv=> ?zZ69srk Pնs@EZbM)$ /^z 8(<UМ`r4 u#c85"zm aXmӅDe8 b)N(%5F$ +?wx\,R9ع@d˂E7!E1% ^tfթa@󭄤ʫ K?A۟>qɨ.hƞjL#kyl֗<$̪ E۹cMܳ _42SVb:1wB*{pwvmH$P n_ZK`vu\85 ˩i^f{Hyޓh%/U`-H! T!fB4+ˣ֬bh~rKfvTLh*DUbUVIˢ{a}~\=52f:xUUJW:=:nc42hJRFX˳PΈwXuI:EVcu Y)$ { oWl>1  Z\h'`8$cY+՚bqBlxƹ 4<ٚ V%醐. ъHx s@Ԕ/};`??Ef"' lTH@wRgN;Gޱk))i ^/R"\aGϮy0:k#ȐgZ6EҌ )"JbCsG\͒+S P>QwR>o]Zӥ𐑗e$3[OUWbb)裫ki1 d&ߵ|*~wd*ßwj-[&mS[1 AfU\1/g\n(Oc}w `ur,gm2ƁNs-1iD)P9ϥJ µh]Ch{>Ny;c䟞[n9)$ l|⍯IJo6<ʑ\`D-RI :|;U@D\ϧx:5On;;!Ōb[pe;ꉬG Uy]jj] zuꘅZNq`\3hƤJCfG ja6rC{c]&T ^B3知q:CY2v6ʶ_|#[c /:FmqNX05"*O 54e圷9k`s?˓It6wHGv/!8o^1ť0mp$wz R$H ލI.n WϬ? @u i,*ӹW YM@oHsjK;W-"=Y>WhoWjmd^'Y?ă/n\5tpZ>lɭj)j^7yY2 PVyyڻj-&_?Y⋣z'Kԯ._ӧo 6A"u |a3@6HBˁ>r5Yꉻ|H'1 Bh25YT`-iΩn~.u*×IM+/66 h$P'%K .ݎKuꊵ`RPg&=RѸ!_d*NkBLh+q5ޛ@`eDz>*EiA+&=TwhӅxľen;v@.)t/Zp 3̟?ws>|K -D5uՁj,Kip }>GKzYZp (4!(y8&ІZ-'6zYEIc{+Kte˯M?ͽ-<]UG>+I]>כI $ K@ܣVJ ;ce K)"9PѫՏU*_^~GRq*Dˬu)=ع(B"t9y}t^6ѵL!*kH- @cO7/~=WbYe34ne? $-K~L nUUV}Xt<-_/We嘶c;`d{R$$#>^s)۴0J#E9پ,-n}\vs-'lMy|6ծ=nK\ {++$B>6N& |{ XKGoiȕUu)_Z@@WtNk˿[LxB#* 8*ϭx}̮gմS\uqY-8S?BKfꓟ#YkT'3ϯ?ge<! #V~TStFGvfPp˭%QY]=S/VpׄƘ{d/ܷgr]D#8fPoOCEEZ ;~+Z9 8  ܚusE [K=^N{A S3ZV5/*s֢@6<+<|oe_Y}Yaw,F)^ʳ˷}EW믿n$1H hύI^wy9/즷 N& A蕯ǶUT#k}H$.dVPKB/TJ#H `rRtcg)oJ Vx Q˔Q(DP)(~iY!F4 //ǒj5tk|%8OwgBVU-ᑽ;`҂h6\>?mI!&o6wm;~kO>$ 94!Y#sfǸ+OG(`L0ZD˰9%4iͱrfeUVPc%׷T,0\Z.h/a a$˱iڻ :;f$P%)wIZ/<~ۑPTIkkRvsZ<)}eU:^VXUE< WvYQU7!*fxdE:$|v]_KAt@mṟ3S$P%nzR6T-œ/oz\]Qj]5mqSmZ/2ŁTY)6LZa]R\< *0c'+^"UY[9m *'+K‹곸zbu}?3Z>d!TN[Zw^'=7w}"]S ;Lw 񃾭#sȸnykg_xzDo_s~* RcɺH9i<׀Vi}۔?_HgmOm1fv֠g^۾-9Y͛/Ps)$ |_H@dS+$/cАΨǧbu;Wihev5G^-I'P.whuX]yDl)zn|UAYm8M+vMY%Z>[Ɨ@Bw)MWTu=J`׮ѧeqގ3֬QL3#2imOmhXVyTV{^v6m-Ysg%PM#\fMgl&q*: :p=D ov\ґ jNg87XCZ<ӭQ̭C>tM% |k L1Fߜĥ s\"߃ Ysŝ  eVJRs1z}kLV4%ģ|m_Z];' ǏS>z+x:uЮɫY(suCNjX\5 c[۷e ٞԾc^DiM6 0OOצEػG rl`w@ӦMCpKfM3̟> `4'>`jZ kqb]{ *SЌTb#k8N0uK;_ j"ܙhq Lo2K q\#VL/vFhLNe8I:& b ğkI^S:w!еZ+/Ҿ {ŸӎW3f9d9I#ކ:86LU%[^ez8 uNE-jSQ!h.XheYW3jzebM \S_*@΍1}'Tۡ_pQMY^yוX#)f"O>O0[sxl|kOиs1Ǟ>͕8 chāD:1tV/]E_.g)2kXשAosۀ8i"U^aBq{nPw q?L >f, 3#:]&i'mƦNR$P{%gGaYH`uoW<g2Zi̺'*gḶ?}^—I[5VN-~EAhߗQmJZ}7+ +s󥕝b^U J@LBmK]eYz"3˼NZl:y_Owht5,y;y~u~p'f!'܊1?t?34E*&Ӻ'8VWF8},<+6oVr>1` 4cTfv요,DlSB"MlY&5m$P%nzR6D/F_Ż8^b)&a^TNv@h=b;Gr*$Ҩ.+kmu^9xb f5EP +5fUGGR=-FK/W'-OUtṵTy5G÷cdѢX{X6Κ&\Ez.ic(,UzMiXMoB;rV}]hk 4Zu۟KT,.?f=һQ!9R~~~/*gG/p͏/Wb&]}ߦ1;\5>Df>?kXc˦"?ffeq,Zd9P{z'@v3bgʘp{\R _]>ȵ r|zMKe~xk`pe/bxbF9y> Ye|8q/^ÌwOu0̾L"\ttS^лі۹x lMPcm)#^T.E!ɫpxM"HTbkR$P%nmvR6HZpzP[M#/X߉Vnө1Z+oc۟'>$mW?>%:~s?~0'?w< q=J~#o,~w砻Mx^8/qnǴ3 Xxz _\y^sJ%VMy[9oO#nu̧+⬇݀u%{t/\{98y%t AYe_nqbՖX`ퟫY bIQ}ukv[@QN_U/Du6W郝&x>xN8pЕcUX>jq8p.]b&!o1lmJ]31wx.,)@)R?< iNؼ K/['~l ށY!8a;z%^})|ޠz9^y^L[^z ^Ǝ}a[b7N_ߒncnxQGl =}e(7[OvqJg0}Չ۽Ve(_R'WcZMw`Ū LaG}ޖE4UFz:5ks ;,Q{Eot[cQQqVbeLwSijҴ<1SZ荱Qu#>n3('O7։矄Xy^]<={Xg+1Ͷ#ȡȟw'pѭ} F]8>x} Z|6`Zj>h|`7K&848c:gmպa;gaU^1 m՚6nmj.4;XsfQT9\.n(4LWQCуWC\ypygǠ r4MЛMǹ/d½ucyO<?GЧ oWϹ.fdnuJȬQE [ 'n$PYYBޖt5 r^O/6m]RՍ ^ ' (cJ.Գ/3/qLidjܿ/A.[!˖UysGSN__]"-=8q3BثLshͷYo ƤGD=O^4W+M^9(4?٫q@P-G՟peU_ʠm,T <d&:uK>'_ʙ'bPZ +D~8$k 8몪,/'_y&kDc_?=<7m_jEdܼ:T ZVyEA* \cXG_D",щ |a ,B0ͪ%5߯%Zf.*LF:[E.B(,"]?,:ug'| TgqAfRrR[q 4l8WXBtB@m@fΥ>% lJKKrEa\ioV]r:q[h3v-i HC L\|wx; rnU_-/]?çqM1䓁&uȂ`o\ݘ܎д7 YG5%|.s,rW'ciU5(3,jTA5u3ŊVV!$Giq%V<6bw[EJ۴EssSޔy Bn>,A5Kd=_S%?b/MԴU s1X+x#H8e8O-FÐ߄?@u5MZ0T yݠvDd:eBZm9 <طg_2nr*5IX [,z&Ə$UQ'W$F锭K//b.կr~h+⏥V `Kۭ߉'nEF? 0u X].> [c_^9 ~ Q>Wy9vL_XkNFimMY}aT5#R4>ybksط5BR͢sB1a05vg8w{}p 5P?q0j)@9wixm;v}FB4*]S>~~ts]Qނ3?桤G)mU3^cN+_šֆ:1K,xM41^XaC TWrM@76EVB@ݐeWc[GtnSlN;иsp 0c[.:'jyg?g>7Ϙʫ;o:#ުȖ%W#ȯ۞tKAkSۉZ~48Ѝ%3c&O6߆&MvMUi.wj؝>{$sYx+niOcˮ%t |&; q݆ڂReX!~4|T/{252C_ ԅ8x2\2UCg,"}ͮ24üB1{棶/?^=g$8:x,6;}> Zp2=1طf`,'ðuX-KE[WxCp[}ope1{o=Ƨ3u",Sp h x?:OuDR㒦פL@A6iB*|CI=W B~~ob3qw`_oWhڢ9hLMhGq5΁>ʏ% ѣ{OFr*?S*".rKpө&(Gr2k^}gGg Ѩ7(O(Bn 4d6uWȌحsz WRѽL_΃:`"gP5E <}/Z@o=#LRXԿH>ؤ)^Zk7DVETT 4oC>~o r窫k9 RYFclɜ(ki ٥J ͤ~}' tg.A-]AhP=8e3%ըv-9Y0!B*_ 21|oO_3AiDJccڠb#R)=ճX_-SJʕoeA-&Wlr00xRtzB@m@J 9ڗ1+ll7.#E=TzJAPZstc=|Zr0ʌ) Zy PjeeWmU˲ t9)S<' BY2J3A(]E"\YWOo$*\< ;uZـw$Ŕ~܊pnUmD׹DZ 8l؃QmqZbr2\?kt+k-[w%l Rm N IDATw"1yI~v{YZ5čnf NlzӖr=K$(Mz)HLQyFxg3+7D0k65"͸* vp_` hfcW,v[ -cT I[ 'n%0rH\u\گeAY.LsW2+Ѻ"c=)+PIѹp䯇̲kTكYhL95xX 5КYJfNgG"]:~Zȿh!v*{N#âW+g_3_kۉ}]nlPn jzbcԚ(Cf'T?O50m <Esi]l_m'뇥_.ʅQR1'yqj7$0V!bm-#O4E+ xH5Y[/: 1):FAtŖXk9O~D\ٯGN8IJ Zդ} ZE WE >lgsֲ 5ŠL=PX G7Hs\T)6nn*T4=N-T6az,s)eoY~xg7_B%e߬Hʨ1S_:5ȎM1V,+|-iRIdC 05nf*9GX˗-Gܵ;wR׍Ia ^[9fcl^DY jI,bT%cuU@X;7pZxTV/l&^$@*5@@@4)"MES_@,(M: "M@BBB ozvn}̼P ƟN;sysgطľfyvVkRh+j}Ą\A6 X+EХc]eVa=oSHhH@CImҀ&=ɓ>!b +Lhz{'?>Yسg| |&D[M&X8gCl;Q 2"/دYtU'#&Y|/l>4jI"$xnv:[aa$67eg/ե *8 gע'ʶ k2zL-Q=X6χ1{ަ%5FϩϤ^xnփ,Zo2,jUM<>'dR&0NiM6Vy4SDΡ^tF+4xfL_,˦;iD|5hx* I'ˋX^)?ߧ_Ų6Tc T)^Akp$ۣ wLOݽ7orhO~KI IJad=ʹYʟ&@O>x mY^P^k.StEkgvK_{VtڇN_W,zfXށiֺ}G4flAv3[2q$lr\?-!?kzHOth#I%  p‰xᷡ $@59*g|LuHcy(Dh+Տ,:',3K2G[:g&Snay`T 9cf6e!; w_V򎀷NûNG?Zx?S'\0W5Á]7U.7mр]wHל)Kף{iWweu;ƂF+YNZ\zp6 ,u(%=@˚G+}:6jo;V ܧtJW XixsP?E809v #heS#C'Km`ГmR`SS|x3ʲ zL:F*H>zf(,W(7a/4kh XG⥱$sfzHLSz{09B@c{J4%wh rxO398xuˉ&GN,zr' _c0wqURHGue0Η#_ꉖ䪟SN:M5+0At*iAAL( ?cޫJ۵{XjgWQ{3!?yÀthKQطuŤlދZ)Pm< J}6\k IE 6o*ɹM8cy\aVul#И 90:oit8,V:Pގ>c](ϴ*olS0>}/66&.%w H =;4ҡTE$>1Hjj tJKSWV߻;w(/DGZyz!Y)$ l]7fsux^vmIm}bx1ꣻ@ѵ2OK:e uhD3MXT|lu#xgAr6@`5֪u4YbyloygVBa ]SU\j;v:̝|M!ih M%9I[ƙ_<7C`zx;E,z|Ӡs5ۈf> f51#u%Mk&5xl*fyFyLMN6aޭemY鏴.X=пmY:XV5׋Y;Uxx?\|XS8ynxgu={.q;PS'ȜBҀQW XOeg%/]~/!TuYyA*CLp^mC^W57zYnO:t;9׽'>z~ \,F.vv-JmUnS32.ݘB@#@Jbn.o`ժu [p !jtp@T @JyDEpk|9 41ul3.[&o@R\Pɉ"ԋi b0` -6X= (4'q>.9qOpXNb<7Nhx{❥1Ix*YbɌZ7C/Z*Go ZBjЊ&7oN]j# up |g^V_Zn+6\b_#݄7O%J'|26/o!<#|p{gU On`U@`g1@gcvJ}2[^T y&t*lu*.dUxGE6)Nh4<=M]={׾ \?ڛthwI I+TXfX; s7 < 9 a q7@JՃjnFZ[aȳnLfMOzwEpWWWKge/C>ClX̊\g( t* X44,+KopU9>EE=ɜȉAmz'2MpNW6OaJ;υdB^&8Y^-92-;YNeN3eskNXFy-Mgn;m,֟Ͳꔦ& OW'.ytDk*6RŬc&.ĈAߏ>Q^ؽc)s6Sfc%8 ܻ4.45pOw#Ӏl;''r}6+\v_z, uv[bkxK=oBcWP^X `>2N:5 eͺ)t.N`a+mڈ\nF9B@c@6o-ɼ8#qmw $!7C]d9ڨjf-^呗фeMQwtOˆ?M$3T}.3r@lW܌?P* xCl\ x@//&Y+3n۫Űvϴ mBm%kU2~X|BֿgO>n7i)OVޠ 7^w=/^M)\p)h@1D&VԵh"OnVy S@&c׀ˉcX?ڽLG?2 yd~/e >?&@m\{5pկŻsUxI΀&@1DX.p3Óy/$\@km@۲謍6-gR]M-'1~ܡ:0TeUXQc0Xbg&Ng,-G/+=XމFNVs8\A'ʕO+έp:7m1xjL?&X:% |?x|_B}uW_@- @bTR;miW ve# 6oȮk|ȗ}ȴ| r޲Gwޒ_הBMX}:% 4V $ׅ%Y#6+^|o=y>=? ~ʀ4%D!TOeAPY!N$ E'R-52':NM Xr3hu:aAxx筵xkѲe1?u9Qжx83r՘Y. Y}Dy\.Y=?GH Z\IA`W8igݥU!&!\a wr: ;-ܹgJKhZ8:b6F?]Uv;[_{F{E"#{^@岠(rn,)^LL\^d?M!c?,`ѕA@o<Z箘{rY0Sc@z7o/ɾyq㍷ dՎ<[ sḾ847_&3STe|$avR}eȻUZ5 b>aBҔ,V=kr O>6~~D7 ۦ׏+v=3WWS*rb (▫S<ǡ1 UM UWtðut*N$["0Z+@Xzuɴ+W7W y0kk yq˳𙞥nد{s@Ѐ<8=er@;/钾9)JVc1s#}zk8vg׷iBpu_>pdI5a-Wn6yB@c@Lo:(,[`bIhߡ '$]&a5M.9l /VS 7ɲI$;B<,޹!Zp^a1/&-,?[tY+xoׯ_n7..hwnkǴոz =szpmK/I'lb/xZys ʴ.7J>5^_`LAM~VNe xf$`TԟVPE7ݭUo3,Z/՜*3ԖE9vqm"tV%[/(H=~o;S)Bg2AKpߜeзuyncN;tq]gdR׳]e.*7]vf#W7k7p_肠[_v\k*ᗣnÏ/11t7Ʊ%?2ᾣ~';],[@b8ir`8GJ 30dވ'DY IDAT'h|Z\3;&>KӖfCg&MѣD(.%={XW+q;ы>=khM&8Gkmެ&H̪LTfVZ +u #6YWNݡՅkpܣ в2i¸ MKǏϼ#GNN`Qa@2"26SwIߝ88}8ky>|jV, j~-&<+YmօrcXp%w/n_J$ 4 $4O/!Zj㎸ ӧ}pWKwghf8d&-5 kG>̌&0QFps5$ i.v=&.)<LJ䪏0;qH =Wm0%t}{ǎp羂]}(ٯ?'8Cy *Sg@Yj^-|"3XGFKfMHkQmon~02.2ҭinxiaЇ/ha*h6aL>NMԭ QYP$Tf#xVS<is?]bm=ۇ+NTEkpC9IR^J;RiZ}`<}vQ_f h9P/̿]vU6T*?/]nU1Td`ҙ[XL|I2/w뾼?wן5W^N<)#JJ$ 4% f<襒M6aƍfZ 8c}b,e@Ѐ|WO?R#su]t94zsg 3Yrk6๟wރ8 v⤁tw}(/mۢu(--Eqq1 97}a?9'h ̟S'܅E$侯8Gi3JyY]9(m&! ]o?>RK9o{\[L|,fcF_?V.`D;w@{XwT73ou5O2bK~:SnmT[9 ܚ;ѲǾ|VJ7{WO ;Qݱ>|9~?V)4i /~:tԑ{1m J^ౠ/%YS:-iNYguA~0BvJ\x  W#wK1ȵo+E)ilZ/9g7E/R`2Zx-,J+ĺ`ha YONqO%V<^7n'?ITY][i l0h-wVJrf}H2%S+Cc߉6҈ז` CVldNc{ չUXcKkIprܮH)O9;ej,S9XXڭjcy՘ B(Ыk4݊ B,۸a^a*^ \?i't$nCfLMJ}թ;%C<3׏3[c*k饳&{i}Ecr5 ͹~ q֗G d<S8kђ#eѲ-jcZ77!; 5܀[]C>-Y_܌ZYyҟW?.eEUPdސf4QFn̷4!38 "^_]@yrYy={Nmp_`T_Rh={696}F 8V;Ѣ T˒ƻܯt?*$[2/H-p;g 3IMCy77MtJ3}{ZtNIÃ>/VZ: 6٥gK0iGf,#=]b,a7qbpE߷>siCZي /Xo`w0P]!xǖZLLtNU* }6#-,ŌF6lYy+c:KDZiՆZL n"/B l&Oó>Q_]Omdc/Y׊:l3ϑx}R0PDz6s0کh_|8#%Ȏ)Nhh5nCV<;4gq}08\zhhtzNL@[>\$ưv'|:ѢEMLZyk;iQ!܈}} 𔅔@7UQ x/eM"P `t"`JcZ_+Q`1K9NYA++6+Ef=R $5PSS[<з=c:מЂ\.ίK$\'}E//`CqIgc%r8d@@J('Ni<셰Nۢwvٻzj5[oclY[[C?E mļ1jiƨñ枠m{v_Si͕(F ۏ- d2c èuf`յEd*2&ZNL6hs|/ ȿlkvispM#j`WhQZB>ZP W6Ub#MKVkVBK ة?; IIMB 61 k@֜yA/7]]֭.-mRnAܦ-oz[7Z} ٩WMĔlym9[zA`T5V: Ch2`VVZe}b:COgnC]"OܭE ~ȗUޑ\,7M h dª5/3b-E[FVзw?{>׏[i4ox{ѽ>@++0׆<흟F=ӖK`E&=܎n 2R"i i i`Gd\9iIM@}ڴ/vƲsҽ;`˻ /.\˴ynk ,L+p8+XY(gMe;-hr?~@\u=Vw__Mؿ7^GP>`zxV8s׮8uhp#v6y=<:wtJHHHf $*K (~lNWOѝ xdJne f଀J^y]>S΄xh 'H: }XDq]+pYCcǴ|./]ɠٳ'Z.FכfGbWH!i i i i`4+Q' $ 40 h[肥 ccʻfvցi&qV@4Vc( %W n6N@$[_Ǘʭ Ȇ>bL2k/2aDR{703 lLxp>:R4444O4V'*i i ih;t]׼^;gT fk[?S,kYmUiHU׼׮{p2Wu1F0{Wm6[PԾ;J|Ќx,QLNƒsi Lzjp8sr/IqSZ:% $ $ |cvk׏+{UՔکCڕwTЂ  fWeY/RE?M/-t.6Y< oq|tEY?_tGwD)J۫k!eFwi1]7yd\,|g)Wl(~Kq:' $ $ $ d4nF)444P\cj\=u~?c9zpA\L _ \*-7FD*@ZՔnV7/8~t -8 #q?:wGՌ |;5߈9Tbp{ [i2VeΔf9M>٭U!ʻ_x|!_jW܁͖^3444ck }aqA]r 1@q] }y wMϬCPrYy͗6'<wӂvmʀw_0bƳdϺ\;ws/o1?^UhՋaSk/E@z5Ǟp8_? o?\ϞRHHHر5' phI+YC+0x^[Y'[l4@*i/eb^@i >R QԤǿ ~-w\)ʶj6`|,~Lg)6[džuaZ3xj-˲<Է,Y`<'>j`\qܣ 0YzrKy)$ $ $ H@wGӘL#:Caكpmf}mzTjWJ̬55ZaEVЯBI_`2~p X?} O/yKv!.9(tv*+Dp;.k{h܂ΙlS,s@@䣻|iIIX ZaK p5Wlҗ|bҊjXt,gfE~ 8]N|ԗwG/Gu?no݁q(ܭ W^wxT腡5c__uW}uZ&`;wY՛6Ec-x-lIЪ5jm_>i i`;k`.%бjC~3}b(ۉ6qwO`2QBPxww'>>={bE%ѱĭ'~  rùO /עzJ JZjKa_ Bk -e mV )$bc{U n9,/.DB@@@@K' $ _j8O@+Jn7\V\ @*_%_{L^KDV|^_nQ֟᧘أ%FL=np9[_G Lʻ fu˳/o*ѧH3%U-'/\ZeK5445n#ҒII] (Qj. v+p7ȧ|y-B Ch5i} z=mߟ@L-G=y F~˔|g }\16*@k3nn-=@MhA2~$o5,\&1VA^zU+gTne'u IIII 6o+ɚ444бe>7;=xoeveP\`n1Cl5ؖ͐+†INYbaSfasϠht.m㿎S9FK,e3&_fu14Uu0, cV;o6at{̀h*RHHHhH@CIFYMʎ67Vi+PL]Yxۂk:Ԑ)Eaf77/Ds"Ζ# i7_+\v#{r7=q6Z_}$nl;yb5D>䘀Bx˾20+aYXO}n[T;g[L+q q>oX9_lKV^5/ybXIIW]J =;b9pݸNuk6YPgVWyMR,덽gp1QWnyI_è 3<pSL]g}εEl6ϴ^ۙVSvwup߬ؤSHHHh`H$qpXv{ӛ+њ5ȗWKe@mV =Ɵ$ؖN}{>wC~;I#kV֊l,A(̺f9@qg`qGVrZy[88s8gh9z.)i i i࿭zLY6mڄ7 ׯǚ5kj*TVVc5'㖨v, (>Zrrtѭabӗq-Mƌr@NhyL{Μ~c1x0ja`Ko+kE#7 =.'L5P+eMl2#~⵼Jkw`ѱ9T IDATQT>M礁=>m۶hݺ5JKKQ\\BNIII1 []*q (7+o6Ez:5 JA8iSQۼ,ldr(݊}A 0&\u4L[/Y\wmys6ӀFWƺuvZ5krѪU+ tR_@Su)_-ilII Zv{wŸ1+k!Uh.\*KK bíYׂh563u\%T,[rY?d7aVn"?_ \% έ{R,] /p]_M.f^5oc&Fwۋv_A_y<3xa9XUw䡀vIj6v=cnG| , Jѫo/g}ЦM*kb4Д4nS6X qܘAq-J ۇ8Of[G:m5!TKaLNE ì0@ eL`W"6x3BDfẍxS |;;#mlmttNh<г 6+I444ok`F\ 5cYAbCnywou.Qgfw r@9o,DЭչLPC uzk6_ڥKFƙ8k }v9?WWW㦛oO~zCAEIbBkѦ4,k3^}c57὿3y7ѥK9U' 4 lo+W]^8݀;fTbw,[ylI@?֩,{>¢e.ӆV&Y[oiC`VVᰥvoFX[$/ywQ:Y8묿OrzlZK($]pݴOt腲˯]{vo ߸Ná!(.)" \L> "UG?]skoV@!yo8믟5ǀ3Oòe˶xƦؾ$o@@52Yy+qN7VޞڹȚ*,ZfFBe,姐UMx|˔y;ȕaOEY7;˓F׆/nX; j x:#_ڣ[iY&\|n1?MO<Z-KƠC6V&J  !6[&Cn>uTM>|UTOd\׌iu+Ə.|ޗeFNI DۢnbIIIF3V)|+ˣR _X\́Q)iЩ| L̊댯^rGr3/+U5xKi=ޯVn69xCAmqNml{/^/q ͘~;` ~sV~Z-3+{P+2"`9[D% ނWֵtltؔ3$ lYO'|pϞP_?nE@ܖoNܔOx?Ob]~ |@9p <;ps]$ճڭB>?˪^v#? ㏦On;1d,h^e]屵,l':-΍p7XpXcBI/++pdzj,3]^+'_AM0}f/_nNƬtdOHHi@VjǸ Ŝ3`BR. ]&:~P2N H.+ʌbƴŁ.ӆxzϴSylXi[5hkoa3v + ঄! %l XGq`#06K3SnSCy'Z4cT9_DbȐ![t2IEɢX$g@@@@w.p>nrl'l݄+lS*sf\T ڰF'Lѩme? mbȄG:ulEqU`,$׫V>Ci=9Z{6DYly6^&(Ƣ*7P̼\\=+7Gs _5nc֒III DZs[/xhJ< CXVL#M|i-։69mhkL{%ۆ.>#sn{W]+}|FhK5,rI VfWh0ɶVs6|>F#5L)JJ/ZCd% Y8w>=ܳR&ih M%9zvR^wGH+ckȀ]e?ʀVf3GNTe{I'z>WUA6ws\A`kʃ@nnI;tf@fU!FYM.ZN lrJڭW[@*SׁIESi._\#dR^fBq]p>Z IM 6o,ɛ4445P@?൓b.Ex` n uzj'i@5TK@6Ɠ|#ezmMNؿub@ց:AvS6u\қsTگ+B@c@K& $ $ 4" o[bVKjlx* R X^}cr|(, 3#^~.ul_Nq?1F3Yp+=:TZZr,/_vk@K9T@T`4W)Ts%ʳ>7;xzw[^Ssiqil[1 u_*'J礁FtLHHh(o%ZzwPfUy+ j1&k@8ezYrU]VinkR5c&&>;8UU\ X͇V~U/YHEK"HK:O#_UFE:B].\+\ ->)#j)Y>.F2>mstW|͙Hh H@1|KIƤ&E~|,;w.Un3cҢX[Smv8r**Rc]J[w>Q_5s[8 >~%ʚjOG+DjZ TuVn<ȇ2G`+[ؼ(>X*X3>N1W]Ø/O~tJh 5IƤ&"ZyO۹܎7mկW➙F :}ʊ h |`TM\{W5WM[lӬblÛe 6cs/vI0x:xt0L{NNoD6-v7 |NhL ֿ%Zֱ@sI s]]ɯr+^ʛ%)A<)3ؗ GG)$ 4| $nIIIM^Cᖃb Wl`x9ff.M3xcݽ:UV{xz*\7u1;cPXRs-ՑY457ZA=q0;5Č6ǒɕ}UُhaV`9X-vj *d))VȪvZ!qz->dӉ/0vpD/~o0Fdm_S2i i i`@˂8kh9NTV00O;CdU WZB܅jpmCGFݻnà{ g-vi:}ԅ, I([=c@0'lZejͫ˼^ܢնg򒠉RwdSs7(躥wR6X3ŐnKlR]4Р5nzpIII; PR@G2@MD% h(H[ڦWV*-7G3,,Xoi+~qf-CWO'+pC@[NBNj0e]3vV J[YY z>hU1M^OWQJO ЪL}KZɖtN@nc"e&IRG-ˋgL<8餓% 4\ $p$Y@@^_~m*eX@b~,$ԕ{t܉ػ[x >/ > @T|,UXv`5kUiNE&A2,ZE@k%O0fWiNAFs-̼YˋոzE!: #ꊘi;؟5eSC@IDWaiU-vVtORfn1n fxyu-VΥTKO>:j\ " urm0[g ldp4-&,,:ktb+-z-K5i] %W|ա:7_/%?tj1]q/a I ^ 6( 444ciЯ-ތ`Q`K v3Y?أPIs=ϟwRNKf"ThL(5kRٴO śTW/57PІfyUC {WA[k%f|^m),L{]|,O\m x1JiJaE6\C޽UB@@ I% $ $ xX8{ ֱU+0Kj#&mzaDmR"? IDAT[m xa"䟛-2m,^i .mT]+@2??+T_kg)?b~,WXǠr׃8ps] U$.ebt{xͪ(m_L+i i i`GoDV-i毩7bݦn8wX9ڗ|kԩK'd;'LjV`u\ZÈXy3]X+Pys@,E`^ @()ib?6>RAX/X29WY_W3Lj 6mSO t)JhȹIGW% 1Kͦv@y!~6>ׯ5zbRgFޥu+VTsJ'$GzLE@[ja ;9Xca F [XH3 6V}ւQJμhd|Ntnُvֳ4":+zR4p5nndIII;^\sVpM8~}+vF+L9e}Z9` M9T`(&Yo#aʕXA -P޹ѰYFEgnV[t@Plr1ʗSA#`q:`A8 xCnmgOL:' 4` $ۀ$Z@@]vmQi~;*`ֻ3.f̀_@*"c C:+iB,2`K^Cwo0;1KwĒeFD\YfN̚> d- zLW& > BXH-?4=˰EEY K!i!i ݆m$Yv` j?ŋc 0 g@U][9I,u0dA^T:K{:u2pws'Fjΐ=kgʝtF˵O"f>"MuL0݀xcK_cBs[V6 >ھEO)()@ @~lݲMWĦWAJG  o֗إ aBU¥[pkdu"R88vԺ̕˽ rhDyeI"wLqD~2bh?9k2%qxE5-Okw/ε^3ʫcj pnj 'F$@$O&8 ؇?VQz>dz^dǖ5ZQB3+!ݥ{_(Zv b]g!%[[-YCzE<[5KubаV5F5Z[ Z|1-$LP^l L,O&c:t= @\ @ TVWH}]wE҇<]h]KMumLj8D]pUB۴/Jiu9~~ܰMT;UZPՄ~Tdsݬa}.G'p qnѥ;]"RKGÉ @#K䳺BE!E}K%x' b&ء,]UvK n#dcV؛ v&_mxݵE'UE±tьuL5v+{rUFZk E="29# YdjB;{EWdݢ^_E TO/MHHH``Ai § jg(vX X7pdD=˽YnFۯo{H1FtSp85  ׯlZgmoÉ~,H)W5iUDH,Rx5ǵQsxIY5Âbj_ʠY D)hcQ[ܚX[ufp3WyED7$ QGndMrg͹'pj$@$@#0tPܕ*ygF`v6کE 5!.}0X?yCB3"KcXkqzs;ooc[=G9|8-d5|muC8OcyYҋGzCh r &H9.]HHHCO>R^ZO ;<;Bj*g]0>H*r8^Sb%/$Xw8^~(-K>3)0NS~i m1e^P[}Lvr$G(–XC.^H_/~1C@: 0Y #GEI#z-"kq tT̓BLB׆2ݣf CNcK!5h!2pcalh+B~cS;lktVӲ{,l2Ȍ㛰 fUz~}i{9њRMT?NHH N=FV&+r"!ªQM".*zHɍ.hKfekXmoT=TZDT hY'3P>`> IhqFKd1WDkA}ɔؙ-LFeܑ@ Pt8?  3f;V["ĩ)l4D7% T$[GK!E  e"QD!.-ڃn X`nbz6 l8Zd7L,= 0`.֏ƳQT  @ r!2h@Yb۠>`6QD?[(a[5Zגvit5f1Znzvc㻴ںWFuY܆.e2[`485 nQ<&NHH _u&q?o!jR%B$n?Dc-&0&FzH^^7Ɲ mE- :WL"l "Mc~TP7∛h.jZ{ٛ+emrglIPt!q$@$@%kWnKv׫\êݙ"Olа %D0H-STDWϣ`B=B٢B:IuV7/!5=B$M\C=sEڡ’5y_6j߼%|)+I)EH$@$WC a7~W/D67;MqR1^H Šzad\ I_PU޳29F!A1qnkrõɱ/S/Dr`K= ׊]|Ş# nQ<&NHH ~ms!D1 H?@YT AM@(b-UTE^K \`~H_CPm~!!j a͍Zd3/fLJB_]xh7kb^X;oʄK'Hee%0@ͣDIHI`rc߾n2hlBԣ AQB{+d8-dqiQ\`]X]xCW{r qDk):qu Z 2 خu,~~8ܑ@1BbzZ+ @^ Æcȁw\u<t!&/ϥd0ؤ\ָ~~QTHwJ;m6e%Zk2Cϡ!)kZ+H!Y[~+f.'!Dt% ?-w>TƍO F[0HH WߩyOoWQaC&5a!e"\BMT)U>\R[*fm?zG;ms|c1phDW/^'3~q۝vܑ@1҅b|j3 @TTTcO'uҒGVkKUY ݗrnX\n&Rk8> 7xD7#-OҌ:|!H~<|MɤWkxƛeРAV #Ftq$@$@y%p1M?I<~Z|.PjԢ| -{ l *"X*#ZGbءn'mAFC,SY a.uSl+k>2bK9?N.R/E('M$@$oO\ugȧJ">H+"AU Dˆ(o"X.$"k{Ѭf[,ODF@<29/N2~ jm:,乕rߩL$P(8{  <ۇ|Dmn4G6 E1Jh(Q = A`z|W>'^0d9UHEM[ԏ' 7]g1Îfu 7Ax&UUީ_LsEtR -պz~%X_b|oBv&QE_gUo( T)ydذaw$/ O@$@$W}JNasFdTHi!M5/DY\T!!\df$D\C '.E9A({>fer ^-({VL2ygPtG! 'Yb>dEsC,[X7XCW#aCP Qu]`І(06ăm&.ц Adõ,m7^#o=L_>{ꉧeǾH eT<NHH z)_#R[.]l^Z-"/A&A A2ƒloGx@/xy=s1m=עaaC767ȳ?{Y^u9c%`I}HHE6]NrL:A`b"R ě2o?OTp|FAv1vau iOƜ(ֈFNڛ6&Ytٓeڜ䨣*C#@M#HHF\n/?<(_LloO#iĚZL\!KTZm8y>6X?~O8OrK乩K^҆!ݼdg$@$@2SO=U~s\pWQ˩7$FKqcEt56b̢ېd. c}k]݅#y͔;iuܾ~?   ۝4{eƏ_?}IYpɮaId5FtuR06$K!~^*~x"In<[lMj]|{[lKec 坧S8: KIHIBuŷde䡇;]n.#eGJKUU}! G6Yk"e ^]AVZ~d̛@}\pdc9$_ϛwK$@$ ʸql[~IwAi9h@O9*I#Hޝ]i;`L(j͙#$iӊ:Yx,\VY+97v=ҷoߜ$Ew~{  D s̑/L}VUYltMz*eҾ,k׶Y- NUrD#dGȕW#FHΝ[(~  {x!~F"oIDATmͲegTWWۆ"ӧϮS~ E7U! _l mg' ߣ[hHHHH (AHHHH M[hHHHH (AHHHH M[hHHHH (AHHHH M[hHHHH (AHHHH M[hHHHH (AHHHH M[hHHHH (AHHHH M[hHHHH (AHHHH M[hHHHH (AHHHH M[hHHHH (AHHHH M[hHHHH (AHHHH M /[RRRys<    Dm]-vZ7~"k | ~)*MӶb=    H9nG$@$@$@$6yݝ;wrHHHHHp n wY/_IcHHHHZXbygf;MGLغu&;'   ؏ vj\rιxv[t}#]v,΃IHHHcw 2$q\uD-޹-&[YY >\0իW{UIHHHH`ǓN:)JK"lD:[ZZ*>СC媫ͰHHHHygأGANGoim]$0!l۷r9sd̙2~O3!   O O.cǎ5Obg/=[Pųٲ[SSc;i$5j1IHHH`W\ᦛn)S7,UUUҡCH.%׏37] 킋aرtInٶmW.1c,[Lk   ط Ax!prE7VTTCbu% pζtMQeuVM6ƍyfrJy7dҥeiiii|نHHHH @Vp o1 稇/ףݍ薵Gtt!; AE.]Ȏ; ] o|Sg;    H,AldR[]]-ƆYt1a4 "қ+( c{n $@$@$@$"+AlbBr= WtF=\rDXDe1!E=Le4$^# HHHH`/ bsg(RQ],SkQף.;6D\n F`.]v7$@$@$@$@I䮘zdף=!GIʪ8&(nvמے x#Fpn ^]l=‹E}l.m6.Bh0qO(]%ח-HHHH˪Bn]~} ښ2vچ\v}.H.쵹6@g   H1O]C.q:{*@ԦHlvkq|lh-Gn߼&   (.ٲ qE7 mܢ,1"E5[b]jqy[\3 ?OvQ#BD'‹k߲P-$@$@$@$@l96X?./wwɹ!HHHH .e,DwWە*W0HHHH %3ͽ۳/b$@$@$@$@$m1 @ PtSp85    趝[ E7S#   h;nٱ% @ PtSp85    趝[ E7S#   h;nٱ% @ PtSp85    ?ٓC'IIENDB`amqp-1.8.0/docs/diagrams/005_direct_exchange.png0000644000004100000410000045105413321132064021437 0ustar www-datawww-dataPNG  IHDRWz=iCCPICC ProfilexXwT˳.iy K9gA% * $(DP HPD ""("*ow&TUW[3%<<A@Hht~g> 6p@kmm}Яq_u>d {zGy>^ `H\t80f_Ɨap/=V3Bf zzx00X˟  3{Q ýhx؞臅'DG#g%%''#+kM{X#UM(K?2A%KX􊉌K 3` P:6V87 ap A1.h '`L0*X_.AX!v!9H҂ !sr< ?(Cǡ (**&tC3hZ>C;$`Bp#i BaGD!",DqъFw7Cp!D5mZ" 7mm m+:j:!:]:7D&a5zjz2>=(} m)M",CC&5 odFCFo3.D>ыxxĄaf2a d`4ĴȬ\|yBf1a f92ɲͪzuuM͇-mmݐ==,CÆ#C5N&NuN/tF\.1.[C\6yùs^a dzK }̧WoEG!UH)8_<uA^A ÂυT zd'I-[a6aDZ"mJQ 21Xذ8B\I<@L|T-**Q)1%ԕb2J%AZPE:GWLeYFYSvrbr^r%rO FIm|.(L+-O*(PRVTSZQTP.URaRVTSE&Q֨Q]R=H[ a  9->-r9m6ER{AG@[JgYWT7P=H-}5#]Hct!CFCb×FF~FFƊƇ7emeRcnlzάl\<ҼaajqⅥe-+`ebujZ:ºccmSbVmW{=3"1=t5[NNNsę9Ry@%WE4Ƀ>vp vNNqo@{8y\NTR6=M=};"}~%W,-,z-z}›e嚷ro;ni5|uw-="?|X߿) / _z67_~ ;}ʷݸE?D4b/do/I@g/ 8@G x o0vUD %Ƣ?aVSTq[x4LkFM_0EabNdg]fpr y|HYcaZ:Qx&^b^r\tel# :JbD*Kj5lt$tyX Q?5362hqDz5kv7n:695ovnvi:z`[{Gs/{4,A!ڡ&a>qg;cc?S'%*1}g{;W6{Yr2 [XUk׳om9mzv^6]=N.n{T7xje<87j>f7eum}s ek>|b7ogW޿Qj]0{_7˿rVn3? {qLQpߋ)W/TO`A'A$"&**&,N2˲r} UJǕ#T) hjlk.jhw4^+/172:i/$RJZFԖl'h@rux=}ce'łBCUôÍ#l"ݣ£crb+nw% 'Z=}KN,ԞeOǤS2,2Ude8>{!EgӅES' _8T>Z1Y9 g+;ըk}4pj0ghHhXx‰ɌG=1ͼӂb딥7W;̽zAܺ'FF_7o)m~!}gAI_ɯkpp8 y `kw B(@ a.ā2\w`l@8APUC%!0k"[C#!ȷ(ARzk ӄz`;ب⩞SkSWuQ{wQAp ]*=$#7c QĴ̜BRΪ:̎cYe̵]c[@BZHlDp{& mIT,+Vl`Es%9e*:ISZK[N'P^~XlIiYyŴ5YUؗS\,dq3ry{f-GFVGbb_' G}!:YS~fg_.辮\ƭĬ.}pY۰vyq{?w?pD!t qZs4/$i@ǘbJ02Qqjf>Lӈ`X#Қ>ӣף`0HaH97~;Z'Bd3JQU1بxDVtLg~My7_=`:__m#gooro%x@W_zrɈ}5$lAB"x pHYs   IDATx U þ/. ` ;h ?܊#KaId.XX0Ԡ"& ?޹23 ρ{߳>|} Id )` )` 3ֳvLS0LS0LU@.S0LS0Lzn<)S0LS0L]LS0LS0ڦLS0LS0 t0LS0LS j2LS0LS0еk0LS0LS^*`[/Om0LS0LS@׮S0LS0Lzn<)S0LS0L]LS0LS0ڦLS0LS0 t0LS0LS j2LS0LS0еk0LS0LS^*Ш^6Uw(Xo7.皳Цcԥ 5-[U2 v!M? שY@۴KCoY=8VABR iә[~_+G_Oґw 85]gOƅcЮ"Yz]-5[Om(aB3i ($Xœ'&/ǢoV%nSlRl{?m 1ֽim_>[r]3mr+Pޟ]f-u4T]oGveq )!"ag~l;Q+֧O G[4edH]j[VWʝ猟TmԨvW`Kᾛn#O`d}Ẃ"v4 WC^ Sݏ-6HS@7M+[Zx ڵLaڡS8h.#<6n.?#pۆm)xZ0L[VΗ6(` g֐T`g2[jig\v::;ۢۯRB4PzޯΫm+`?7۬U@+\4 |8ODgcF5̬[P4;saҷB4޵ ͻ}ӥ\gOlAWl.X?7/"VмyWqP RKY\zh^{87>1oY3.ȍ&;922xx~]g亽5;my-::"1uyh\ Src*ojbe3>RQ9H̝2>!5C||vb5z^s&/Jn3͑=6(?21qD^Q]nbڒ%Sr{(snzﮑtkSleݟq%KDZII=q^>_n-ɟݴMmLgZMPUalU*@p4I\G4ɝ,hOy\>.I~SىȒP2Ǜ^=1Z}bQlM)\) TJ_S3)IZp^?c\#qeS)ʙ+sOYʊv,HҴ_+b&եk&EP+էnikˡ@U.9R2?̖%yUэLC,SMOL*<: $fCiIN4%1sS3KIǎM+&gyr5FbKd+:ΑݼS\N2o }BRĔs,J̛;31eb /ur/wĤɓƏ`~Ȭ"m[<įR-5_6֜ #''1~y L_2g6 \SmȴIЭnھk:}~dLjR@5S  o%^4Н9Mǿ9<3rc~pǎĎLN؜=?k3|. 3(R?DvbZiҵ<17ƹŅ`ᩔTtw$NM#ENqc:ҵuMI0DIOsn/%H?Wd}9!\%ݚXyiYx}Fk$z2h#?%dg~nCz|x5n~v]j5~*l Tv{1\RWm[^m*oSh`;%x7<ȇƨޥ|0L,_tnZfsSi7b`Zᴫ[m!U>bԯ 0w^JC@bY@^y0.o Eu~td_y2zÆZQڭҚ3<83H}02ޖLzMzmI˘I28U%5kSƏ:tOI+;D Nju&_ۮ:#-(`[oNm$~Njs0:/֖/+g?锔Y|ExS9%@i1}E}S!?weKU2Z ½s'kq{qҖy^T6YթWDVzu6Łcb nz paʃKJs&1#gE.j:LGjFJ5sS)?fR83>oJ@ԧ4BfjX@ ۾EXvuƺlRPAS;Dv,Ć/x͐/d]*țǮJm/9g_!7J%T\L[@qS1bB\dsŐL:٦donrixWZ kO>O?G+aE#W4DGYl)F2oyIBi?LIS!<F[2fP^#ekok5@(󟺊@M+͙2¢r .nsÕL$TK8vzXza0zB<֖3~6n<[uUyO[diłUxpM;+mP9f}u{r@sS <^LF+\6iLιiIK#٫vfr.yl5y߻ܲ /pn6zo /sL 5폾GI*X85wЭ܌U8{huj]U˯xҧ<\wS4*ܙq1^He֕6`傔1T/~q& tY3R8s5]ڭb0EFOcXd}I;wÀnܴCWy("0#tW˪ KZR*g :'|i_qj^qu&ؑѯޭ+t;#kchLT]qו#m/:5R%٥A߈r OIO4[;rV5Swi+ T ClKT8\yJ;_ е@7]}U'Uoo),‚%s(־Kgfc3ŏaMz B}Z/s(L]0=Ҷ,jnʚ#a`u%װ g|X,$'\ʵwRg+ hƏdTZ\3շ\l u$eN(pK#S+[UP [V`֌5kf̘B|cg s޼]z@.[3=;Փ ~L /'QUL'/+.*r_FĩyX/gʯ{(;#6 ժ.R/ ݴtafJ9d TU$yyꉩi8{2[!{XNOUO#F\e޿ִF} <ʚdX>`=[ذ WT>秅,Ōg%S4zQ=h:Sz vos& 8kZFS~dnj7}K3K;#(?%U-rԪb t]gY8,Kx(@7w]oٹWx~ ^Qh,\{U;S#joN?`⼔/DR?/ޫvnM)i4vycJ SnʯOjB|Qj\ .)1u'N=]$ĉV8;3^>KSM6oP%[F֫* ïb*qj})~:~#@\ u?R~SVRZ: msv4jJݚR)g=${z\w l #nov f,^z:z')mh~H~RgLJB~zNHm Lî+'`^{tpSB{{kfodM +P͏6@ ?].De^& Kχ/ %$ZsG'&N4q\"/x;0>2ES٢&Əhk\1[+'ߌ&/0sUUiוXaI˧-W,4J*HR /iMy'&NSl_zN"///1ivBJ/}ueW_B}I?UvQ5&X;wb卝2eJbqʙ/6Λs9%9s'-(f|?ei_-L2l TFf(Sy J.?!O#aoLZV{JPvnB> N-3?6 u3keT %΄gRi$P\=8` Y7/Gnb^v{WE{4wbnI3g'&gbbe35#1}l @3nRb|^o&MȿJe억.޿X.,tAUjRɳsr1vd^ ECiI,T]ZR=QPjLmk=q:އAS : a|ZjzױfqB0vz\DV`y HvT޹$!Uo R>,e⅔DNi,:>:#1Vca!Rr-_iǦĚMSv.&NOI·Ŏ{3YfEʹRoDżSl7{Mq}`Tk9(7~18cU"g7oYJ{\\uu\3I5,g T H;Y7m|9Osg4js NU||׸%ꋯ vMˣg֬X|nVFhּ=Tc)ާ7kof0v6rkl=ǑDN'h ,xODg5}9.2L7Uشn#vfڣ]Mx`X{\;ϝg/+^IU9sIJw'?vA8?gj&p> lݵKNeKԳ]{\WnTެƚe)`[Bn @}P@`lۣP~:H6٭FOAZe=UzzbmYum)*FzH{ M5v3@w8϶KS9dx7Mx01a,wM{vl""용E'ÖRi t+- 4LS(nI_~-48x +֬5xxK=%oʝ)~;vz6(`1L:LSfVͺEV02̝~/ eDfɉ܏aMS`R|椴ϕAةXk[@A]3I[4n=?=S0)P ƳMǜa΢E%[3{8}8[Y2TfB g6LS0LS(] ](]k5LS0LS g6LS0LS(]VS0LS0L:n=qlS0LS0L0-]k5LS0LS 4u'ӕTU)` )` 4hrJct|(ٳC[0LS0LS 66LU*n @͘-S0LS0Lڥ@[*-yraT. tq1'0*6b6LS0LS0jP%ԦȊfAE-**27o3<3g7Ķmv60LS0LS*ТE s11b9kNaQFz PVq%҃_W_}5n85)` )P[oaXR,[V֭[E^[oNlެZ6oV(֭[ao۷ַNTX3+ n L7ǎ)` )` TT~~֬YxUB5W^>/zt^V~s5iME&hָ1mNkkNlQk`%xױϱuݓ CN84lX+-_NْʦJn|¸;9^_8S0LS-^|EX7St\pᅸKpGْK$_V1+t2ٲ:S0LS?4 oqX8Ws}c67N z iVw ZBNza pEk0J@%j pLO/_ Gn}I;P#z;uPyU86C(P]L)XZViFMS0LS*i&\U|G"[~1%40J+3$߮CIZ+=sF16g'rէ^mxչ3wO%BB ,U3^'\Aj*f )` @9x]n5pt9wHB'Vlkt |z[p<rⱺWmf!!Ymޘp^;AtXzu9wnj,LS0Lz?)= ^z ~w@@WW`aWdD&ކYYZyoKAZ8jo(c֭Sz#̙3٪[TBve )` U?vW_+?w^ 疓FċB LQ@8MK.c5c;SPnPYXt-mh]7;G1cssqx/b{pGY*`[ O-0LS w6} 3~7܁fd2YH ~AՇjˮwA# 9]- tkɉe)` uQ{SQ34\x FUy-K{n )q$K'YҨN@ 8Z}B"Wy׻<5ˑ^] ^];EN `w.xb쵸Kwѭ[;c]x=6)` )` >S0d|mZROd@1R/. ŒlW=_ sl Gm.~2$gznjÍO=:FƲ+F +[ Yd )` @-GOttpJlu,GrT*dƼO-66:%cұ{~C`&ІW4AǾq:VІ5l-n|h!~ca6(`[΂0LSc rD(ڍgB !I^K$#.$e=U.^ ULA@aL|'XJ'r=^)ko_?*qʣx7 v܇ 3`S)` @R>ï~KLȻЁ)Al?}ɗθ=3t@Akw Ӯ"uP^Xzm@6&ІږqP ۵ktQ֐ɯG!niö^Ib=C8n?o\}{W 3yMS0LS)pua9ݥq K@ԱW+A cH; ,$$"_4wEu^_Y|\/{HzL tF Ȳ.&i;qxG\3YgĦ)` )P?sr1jG zeY'e¤'{y\EURq/vn''vJ]_z0܂_TK/Ӝ_28_=%n_~%ni)>ܦ5LS0LEq眭U@ %a7z YY> :u!Ui:mk[rڊ`57B .׭^m\[aݙ!9TlF.Gta_c-;յ淙MS0LS(;`Q<\^"N;K%%UHpLyEw2(u-.7Y[(~UA$:/=ʮ.xs6{CpFd+sNns1QWԬ5f )` YN`j؁%UϽ'ӅxPԏu?MG¡ ,HX&m׽B?s C&X6UbC_dY~zh=53VWi(c~h|axG9 3o0LS0L裏0qũ:tWB| L dCJxMWEȌ<C˗kG֯)鑥Ս98?qGVgaIN0? Ag_{j^ݚf4LS0ONEfMճI% %x2 }HMB#_Ձ$iQ?x@T>&%ՖUPjX1I(C'WmͺG%KAVVefnfR]\1wo4͛jX ֠6)` )PW8!$8S_b~ ;x()M$&)y}վLx  ι\/୶Y֗#t`Iwb7<cԐYyUUڌ)` )PGXx1|Cu+V^1x< } vQ^\MoA%a$) <1.Ota JY{Wem39kRj\hrz{ަڒaبSO=>6R*rNj)` @Q/3;-I#1xCE@ǁZ\ERDaPE=,UV`5g8mWʵyvLsܗGot42Ãfy~\u?t錗%R*லf3LS0L:+g~SW@zX tT(b4X[AU_8~:dBN듀0DX'JrZcx]Ǽa6@nxpY5뺸he 4+zue|FpgFILS0LS)ECVo"x^1D}9O^Zf{eA&_L,sC\zNs17W} .°֭/Y^H0(y<]؅ rn&2#L?+ bc] ؖ\/f^.q^# ֈ6)` )PGQQ:i0V=$ y@$j?G怔O !>gH e`$ŦYC_5[9]eyke\N{ЊBޯmgk<3?qB9ѭ &,Yק!BrjJݚR1LS0[lAMeՊs qչ )Eh ?cWB#1𯵻Y^{O`Kqhܹ ՠ5wlh$S޳ g_¯_4y~:v*6gw9uӣxoIw'b/שgKVRUbnJ.qQklѴ S@洶LS0L*W`gf0uVlFХ/znK Qޱ Y8_!{ eIw>Sy9x3nuk1wqO/~ǜ 4ip>^+,vmS5h;y[pvxxKW[7'9yu/ye>#~Mz $`ӳwʷ=7:Nzu{oh>l\ -6mbR)ШfLS0LSXxwc>ApDPk@`WjwGzmN:9 ȯ <(;t<{1Z.egX : ϬڌEsgeŕ;ŀ'& ؼ~ q\bsc1a =_W= c~._$l !? ABǦXv=[oX GD)LS0LRé#вisoaŹض:=$! ~ }E0K'gFࠣO7~!mXp0l {7{3+\vdG:KKv ЕD}cigޚ.[^Htd-Z 2ܸ vVeU7r%ݫ}+J~{2+XR~iؾcGd2կnkl3)` զ@gb˺/pcЦa9d,s% H^-[b]Ӄ[A4KK0fjsظh!V`ndG-IH>ʹ_~mrfev)@o?$Lxq1?71oVNgtŞRY/ken-I6j4눭8͜<۱z0Э^}ͺ)` )P <;|][bo5᭓܄Lm@:#:JӿZJUkvŸ=.]`GI -z"4Ovaݢ3XY(w[`R{js5 ν -r:?Oe![‚iՄ%M(Uomhb93_CؓIW:9 Dlk7ޫSѭNuͶ)` )P 0^)ñ%]10&ڨʼn͝xw1~~LEؤ70r\K.II^c=zO0駣=M(`R@yʟETXX;wbToo>{7oƆ pyW~0LS0F?տ9u'0.%^7yb59Nr%߻v錕Xr7iĕR&|82Il36Q_Yr'-B/26ɗE֍I:1̩GnDd5p4v 6]{Ъu{l ] O諡Rϲ~ vRq˜:VQbyƘ'Ļg/ӦMCѮ];[fѤIdee)Ob*3mMS0LSv(o hW;LM۵Ggigco㳱nsZ8!Q,K% Щ J$[KrLC=gfؾ5Z4rx!!4#3Oplڢ]suzS JHi;aD rndQ@[?/bT^߂Υm2H'ҙqյ 2C )9ɡv t]b0;i! IDATLS^l6G~ >ZF" ҳy)8Ͻy4`q7\aza|GƢ2 e$;8@U!C?wtB ?^ d L tÑcxzI̢y]tp8z1N_ѱOQ@ftYLS0LjUgcxu 3$x9~6t%Ega>}*Ոh":s0-m<+ XģyҫˏK{IY^b,ab㜗ص; w> ӆZqaJFq5kUqj@Yc0Э!mS0LS-o(_B[I"<=rÿ_xoIvX$ )f":1IJ޻ɽ&X#^>=.D 0vXZݏcBgwFcq,L 9ٶ]+ IF.Ԉ6)` )P 3lxq&ֿ 6'ZλTxKE$B:N p@|_ܒEuVn&pXycM^fJ¤~L2|?@l(y9G2K_Ƈ:1ކXt_ms|9]h927]O#0 m;waъpjL&2LS0N-{,v|mzAۮ=kH+`EϭNƕz"pهaYpvcѾ_6ݮܒc~|؁@X_CK.B -R,@GZ8`k eޕ]&dp#_l0&󩷘g\\ r`n4U"|8( 4s[H4ethZ[^Y t+3LS0Y$lz}tqi=刣\$%HJcҙ$iKjԬ:=YH}7G }'MӚMQk]§99mɑ.9JFy5o}= n;zm@0X.s{.=ZU=Ǘxuyu i$%y9#lP >~ݧ[KSNYf0ЭmS0LS\ d Ihۥ;q>i+Hp?')IJ˻ԓﴏwA^5W<®CLƭOv_߃[`XA=mI.m9 +s2@- ¥ zke= I\;̰9l.|Sm+rpyMH7ųBJw @- {:9T@ŶLS0LL lY.b! .e5.#>g9F^k6֫ey׿*i:Vٝ܀' 7]pMRB׉97ϕ4+_"c}c|9&IPM{ 8Jڏ\!6q>lz~z\c ÓOҚV@LS02$9C"x"իL vrUM潮2`1ɁI/_G!'ob),⢋/ѿB4wpcYlSUCl6d}u! _V#,F#O&d948`v|[Y[ζr*l~_j/{ewR@ :4P՘5&Md )Pн7HHB]5T(Bl giғz P]7a ^ї uGNB$|$`CuXϞ=#14WMu Ĺ9ifëvb33~ b̮_xL'4%)P{Jln*VMwk' nxQ83q:&bǏnYѲgK5nKn)` '4$Y]Ѷ$>${nF I*MIuH5F Kʑ gծ Lz66?Z)E#[-eSϹe[n #O; =j,d \$EY%\ &v6pM=Btk~웖6bghIW5^X)q>&}zU[˾=Nű oP;: ޙ_S[nWpFK@} Mi )PB\. =5$uGrF lI$j4k#\9K-P`#iCxq%y#zԹgh/d9ےZl/uuj߁G騣B#/W]sL璌j2I _e TO;0x müM.=S5L|R(VY rm)͟'2 C ~錢qO%ym8wfaر$|@5MS0)! /]m{paRaHGcn!J[K+p> JJ^:1Ih+V$,[z$)yXvg_Z)`ƹ 6ÎuUc3?9.8}4o9=&Y70HWaR>9f]@ ?&y\KU7G[u:jmV-Mzi>Z:;襋Z+ڲV}9gyƍ}>&5LS D! KH|KI;r]P qs+dp$C?+]~r 鸍b̥BDrNmr<-9W$ v?^( XsnX=o~3;'_=;_x=z+Ŷj 8^zn97|$7p`+i1e@jGVi!XQ: I;?O cJu}fO@ΝUXTS*<$/N㼹JمzvT&Jk$,ImR9>ux\BWΧQrM_@OR؝. sױq|;߉@} Mi )P;! fz>AMуHA9PFSIuKhe=#ix!Iy|nL 9cܛO[ź =nWރrwލJǃC97x#=$z]y޵k JXWzn 0B@lp@jؤL&%%VF FhP\qVkDZj]eJ94~vLNT ~%Zl6S0LSعslxqf. tE. "8Q8Ur܆[Q΋ S!V{pe7Z ^Tz]P7nߍh]%]<YX\:o`8}{x9Z_ۍ7݄gL3ug: z bq6%OcPWO;N^^FGPP6J_HPeJ) m:XXY K]ɸ+pǪi{۷ [mvS0LP^Fs,Lh4ҸG>2't4ςTNIOij`|O >]Am5BJsG5Y*JXE<'Vn^GOlzt8z~bKO6y­GFV2T!Mx'Gl\fM\V] 9`60Z/r|G=󢌮x:3K/܋|r~ILਞ]>ݲL `:v#LU]w/ӃU#=|]t%YViμ#i~Lia ,QKWIGnݤڢnm9S0L +$̜>B ]/$pdp]n5,# z kvg9QO# Jo^DŽrPc{ҒX8(rlO: !n' WD6m~ޕΞW;S@nGdD/~K|[`_O,.X! rx^wC;f*u$ȿd9*q~d?N ^\fj1v澨8X@ؕDt:3n5q"FV{0Э=Vb )P IWdSХW򔳒<@HK*ɗ97N \l!d}mr ]ds%=2$0n,Mp~-UʿpU(v.ED1#P 4pOr֏>@?L *}|T{~7j]/o;mXE6h׏rdA_):Tz=tA` ykx/(2v8nl2׌]{WV)`[N-0LS ]Nܰn8P=y9(M%kĢpk#fW=zGI‘mtYEQ=$|⠖UQH_P蕲|A .ԪNM.Wzi_Jh֭ضfxo3$rփ;! 5W_^5Y;Fy;c1W~ <.1wpsh(ꗻׁ%x۹q-v|&uɐCFsNCzH-'^+?FcחkZtu^>|Ï<;S~|zQiLLG2*">UaXOx^(al(Jv|76~y>wV{Uu83z UH.¤?qT1Zn>=8S0dϦ%ۥW-<WO2\C関ZꭨX=>4:RYZ brF*2ߴDrgxI@u7LT+Gڑd}.$a$| Y:7swފ=zмy9GWmw܁aGK`> o}!<`s9Ԥ6F$+R`" &+̆| Gc[poBu K.I c3LSn+gNlz}N]z$$8Q!qBB|gHo.gA#Hr(e1HƑڗl`ɦJg`.HFCY;97zcYW@p! '5Uu-Ė\s9 X];8,~}\a. jTکV4 8-xnQ=ZMNK2y|6{,xEEp<n;~"{;v c3LS)$7l! $$A IP@$<J!4m=ķ; D@0*6)*lzp]hD1 Cu:үoWHk|B-Hg IŔQh=7;4wk3^xO>$s8E@ŅIc My$雞/wn.t!6B擉]L(w>\neC .V0Э[Vk @T IxAB>N><%!pJCR;ȐCwH8~0 t-~7b><[r 4MId%ϔK6%?^d%Z7fiKx#3Z/eI]HHB Iʛڝm.gS!U.gN{-q gRKf<BJ QzBo+@Paşw_9 q`n8 4C-˷7 ej.q?k;{f5ƍ܂o-$ԗs?P>)_0ؾ};mۆ|l޼6lySN0LS*$%Zw? 4U j*Q5.\=AbJ:cna:|Vl]>vAуq\[!F^T.\"XC(4#ZJ\\6Pϑqt!-~q\+-$c9 ,⥗^“cڻ{V!fո3*xtMyk]q#.B\$_;ZҾWN6 ۷cUoٲ%5k&M ++K$(bͣ[.)` %$. NI IPS%IDPE 4qggCE_0m؅϶!b϶U'OŘS²sp957t]Yzpc"XJ۔\gM $ǻ$l_1jkH,'Bȑ#c̚5 s_yoX*iZѽcG >>dL Eh.-2n[Nlh޺?[۠ǥ ý]N8BTE7`ۖMS+! /z%!$] դ-ZR.g[6'=Ҥ#8_ТgY`e^#HNa#c7ƞc "*"UzoRwϹoo- 7wߛ;3wܹw܏ 7q֟ǤO]pXge;:76f/Z1 6^ؿuA. =QKBD\BogϞ%K`/hXbrr\., ^TZRSW2=A2ɱ_K\Y}8|%-#8b$g4鵢FՑ5w$pH$~؏ݕS( A:@jZ $2ƿAN3 JG^CuPjF"8]Ld׬Ǫ9Ȏ\7 5`c 4+ 6^Q52%#$v, 6DwKH.iyZ///#T\|]xk 7M/NJ|`,YeZaEV p^xVdS 1常.<{(&wIpuq@u،_'3'?zɦ|m\zFWwEV> נC LP']Ռ5~[a ۲h3l, ~< Mg |KBNAtIȌ ]`VfOŽ ʻ*,VSa\&l lnWxC|Amؔ2 +OPKySWnQp o, kgIJ_]$ ~c@7v@x x VOA,dzjMz+˩0q0i:5W:`Spʞ]ө|h^:[UqSnøktOsa۱Y`J\$3`k/m' 8Y3Ȝ5\YtU>uXw)8kfdmXd34 L"ȭm2ծJS)0nmrIF&rPZO[E)(S%^Q 2g ѽW:ɢ1ӹdKD&qeJHJ_9^ c7w+}!SM+f7MZ*X݅)*D,]w7tCNJcgTM [> pN+@`b". AUE 7x//o"_%%%KK;]J%PBG`U\!5=dSd6rEcX~@vAFs)Ƽ8eƠJ5Ё~}ZꕊtExq>:p)Z.%* t$JA,Ӫ8+Z m7%! EYp 8KwI|1@Hͷ$$ly}v/_Z%! 7͠VX E B::BZ9A.ewe5 [ [6u<irL zqN Ã3Х{5Xܹ:Ƴ >,\_2ò1[K}^Vpsu%aS, }N@Kz x |O x=ɼ~ Ki\l\Y*oY-.8b D'؋\ș6`{Bz5oꙠ RcxдnnF2 _ƖF~=<1[>j"fb yp];Fu2w`Ttiq4K#vi&, k9Kr7$P.>x x 4 x[{ x x W |KBp`gof:(p, dEԡV g;ӟqs̛ANRԀVbC@ p{W8051qK凉:fd˘>ͲRPtL}^c>bCpˊ@6j0Ps`nXw, ]2@|zIo~< MM撐e$Ъ0jNOGP@|,0SIZ{}Pf-]br586kn" ۏg^{3sÇZ k_GMr4jT~Vq4nt,jջ󑖔 |x͸%$H>x x B@KK-rIH;58@kųV MEnp#XgWxWv9sʍ؈d׸)*O>β OLVEn%2m4C#Ww^J|h-v,!`M6:ۿz% 0ob.T)ҴK9 cU1Y8`!՘-(9ΒuwI#//O~ďKKWw$$.C'*Xkvs :@0ߎI~*Y;JV}aDܺ=0V@7 3@8VB{gĆvG@rW.T-X6;Cs]ZSKWI4F҈M*}-;Xr7?w'2D.. &t#%%H\%!fm ,feC"Z{oOgu 7s7N\lddU\lW%{qU혱Bfʍ1&-\::A~v;bNaQ_r6f߆T'HN)` crOvչ$lKF-ܰ l]BKKHGQc%pHZT JF- k @ o: ꔍXC IZ3uW+@vk|]Y\JFZI[ '(ݷtжQm`R7U;:LӔ#˚DBR)β`g>)pÖ\zC2KKKHGcI$ 7hC, h80*m%i u/MO'3sc Q!6pu~L:C@,[lJmJfs)֕-K2]YN= [bu@Y\s9˥|-fU6 \IKV$D/. @}%%pK#{ x w %!ju>,(dg^7`I^*JnNAὮШfQX'3 K/V)q{pQb\7s%zUh`UK1eJ r'Uq_llYCW+mhSWoŐfpKq\썍GٴTl ҚcA, %h-JQ%pK\*%!=zh@kXM>$3SC3\lcG}1T}>OG7II <4ݙ/@yp?r)FVO`ѣII<;UpUd Wqx zkڸo0lӛzlRCR6VYk͢lXm%$P>x x -@h9~?~K–&aӄ$$*묭6F0LMଶaZE}]v۷ϱ>a0L\5N-1席ITe_,|nWˠ@zb0nByZs?[[4p`Zs:qAjvXJJ`@ nzQS}pwIp[/;0{l,Y~E Ċ+ލ}#%lJ PԪ]9#''~-[DBW}^^?V_æIEn(YJ. %i$ Rn_[ ̪N"2kM Zc"ocҩ6nMi}~.jV[>BJ%r1] VJx׀+-]Mж)W%{-zU߹*C۹m,R %0\FJ}:=Wc例u, "&>x x ={0i$3u*eT/W5;~=ɉHKJB*6K@޽_}ݳK֮/`b)|r%v ]ԩbcb#<_J$P%a2n ,Yn5[72@_VU?i \Yxj7AN?.kTE*AsjɴdѦ]bѧ/UP̮RaڝcnxTm"*Yi6\l*ahZ>egU ÐT´{b. NYLV~%M ۷o&})>cG ЮV-~ Y %dٶXSWb,OOM5NQٱ{_~Q7oi!Æuk>H_D̾//#Kx$̊%A. 2TLWpV9PZMj U$z3熖\\ 3itYl<^xCkw:gB,IpKy/\z\Q#H@];d[;T.ʸZ^*YE9ISp ÒVKع$,Ú)3a 7xJ/.͛7~T`Λ ʖv>k\6; QQߒtiٲ|[N܉}>|(>3K ʞ*0򠧡Sٵkϲ}vlٲ'|rp<=@(]2蒠YJըX.ܠ{ !C0)뭂ꜥB+-'T5/W6sADӊdqW?ׯYwٟսl,%F Lx$dL5/ɤ-NY3\2Fcn$&%ֵ UQ̒n\G ^!˗;3&6* xZ:jѶA}mXM|9܃;,!VS~Esؿy#9, iD.3*zWpВ,- IpAu)VNhiCI7{oO>ܛ|ոjC7F%fK juK}xzBԨJk]Yi՝=x IDAT-n12Βwz@&];eDY| жm[#97Gﳗ.osI6|$ihpt}9 @aExA1,ykU aۂ<ں-7Q"= Ty10\[jt m),,![qZECY%awKB^Ѻ]omۆ3Ŋ ER( ,ArG Q*dP] + y'r&y:x;qb8 oB)%Z?*]~|#//&C$h.ܐڭ_1+nYjetR|om"Y$3fa~ΝIMG^vŒ k1ns,y)<> 'Sp⤹@n٧%kC`(qp GuCXە]kcߖسvP. , c^?Vrj֭-7!R5MG A: F'K<a6Ta7ȋ>f3EVfp[9X[o?cǢJ*F7?N8V^^!]dN̅"z_7X7&Nupi5"t”,߹arG]D\vN8f1piU; MJkM1 rn¼mࡽNMͫ"5BwPXcQ.^JtVN@oA環nn.rI4B@%sInC܏-Zein Ft$\˫c( )*K:$ 9"P魎>8)*7paJLNjWX~SÅj|Q@G7%qIx}v, Ն_DdAScl\+x)z* wTεߟLChw J>Td#=Pm4ea\&44dYab+w _?V6·7X!bHg{d56eY"9؂tI, . e%$K璀DG^x!>zLTjV ?.7}b1SF,g4"RA!PGt)ru_JɩPQYV}V(;wT.~s8}jg}&tgfd$Y}ٿen(Pݻ$GG^[ yy۸_1 Z"% "UFb65 7 J =c!QWKgq;vV}o=c\~/ x䉼> |KB֙#YtwhV[C#7ŖeeGzϾj;z`Iם>hu 5bUL vh?#^{_13BqⶆUq}&0 xY5Ƹl{yQ5J9 N>`:}yKB1"|p8y2<j|p &ȗA l_B_o"K GՅVMRcXX|~feADwnZ^uk"=wK8j$p( ͒`. }yB ,RnjJ]0K jUlVт@H+ ͜6f\J̶sV,bvh/|1/n۱xGNlp,^)~hz*qX7[$ ;kYRv|z׋R{#$. ^9Ll݊O?I*@khCk! \B@҇a^AeEnL!N8Ji^ eeZ Q Ud!O ScWqfF)̺k zL?q98>~8Q&KK"C$DfI 26p/UM嬴ZbWlZ꠭XXn",˫ܪqi$N# Ϗ̼XIZX&O"Snmc5b}6_ѳ$T<dvn|%pՕWb'x.k6 i|P}wC_؆Ԉ\Bms#J"Ei^wFіqsRƪcpl ]*~A p<>|]ڍfI(Ue$P<>x NxO`"%NHxYF gXr-КkQ4Fk~N P be\EAPjKɀc^U?R db6p( 1:мת\O2>l|)#}6 xm^ wI@ZRY EV :V.j,%A``Z8.]o7vƥ #l䚋i]+l+_l >| { LmL=~äѫJwtH;NӤ|v"s@e_M'ܤL$cк>ٯPl5%7^V^ ,fk/W+3MRLhW_A7Xv:GdS zeչ}XU(^իex|[ֵ:bΐt|M zx.RefN!ȏ YYLfjn.rIسv3K⃗'1r MDmbGl j "0b[ՙ?*^:XޔXyr C.Px% Kj@lHԉ IZ|..DA..Ѿ}{p<=X">%pHKB 6B:nX,?Tjh+))y8 kfz]b.n!.߈]NÝk# .jR1e8Xܻ>L/LE`?B&w^,;nrUC \4xW &׋s nk^^G֭[nj70k 9mf[v&L+&l z`?Lۚh`ERJh]^Uc4Yo9U_g6ݬ  ˅㆓O¥#Gb'p7; )ݣ?$$l~lsInH*M@Qn J ȌJŞ- wz!:vuKEuj;ަuxEpL'\Y& cyWn%8)fԵq{a\4f}qHǀQ-1y,/#[=o #vvdގ+ĺ(slwd{%x qfUJюHiVҁ̻ew䦠U%UV)"b1QaQ,)0foiؠ\nbEM>9%g\c⹿,*GiS;vMG}q K9/N$QV%:|$p,82O¥2u ॢtmŤ%RQ1nW[3UKO/UO_B+rǝ Kcs`39o+F|uZWL4} -ܺt=.Zƨvt\`כޚ]hST9Y Q.=Y{64`%K7)3gbԩ?3)k^Rm4W6 ز̬tn8{ZLƁhb[/+&*L5jN>,7 FZs3Kz[#vjU{!E`~w8Sh ;>@7 wI(>KB. T8JG@KiJViY p>#l֘Nxcw> 6b,smsyѹj):cѯMkw`~8B ?N۰cY{?7ǻ?#AY -p"ϊlkWx'8*$pV4ɜJ,N0 4+B UO\r T+ۃګ0u%n=.M.a864bLAe4$Ufg[Fxp׫؇@SK0$5s۾qI, 9NDJ/*_ ([kB?jr6nE_~F&c8kOZLR9gOƣm+<=C@ICL#ӸF1jd}޿m v] [a']zx > >'8t2\4BD"@Ï"!p ,¦˃.-NW00aY,xSglʚ>cdl]!N};G#Is `@ן^rIE-PU]⤤L1 4h$fWuP0.wbkdZ^_ǔ5QY2Up;yPn!5z/-?o[wǨm,xjIxɱGĎhڸ%x2̒. kKoC 4  G[nBB >h)Həc|熈O7|,:@Xd ˲+ Ї V|85Y4?nUF.VTQeKS;2kn ֯Z:^6﮲Tô$ۢ8 x/_H. [ޜ@EuF庍ڵSԤU4LYpӤ,*sW͠ <3RUuk7PD&N4d)<(h. plړhM iu.GuGOA51j3*r eeqYg>l9lcrx+\ɨ0ƅ^]rc<=PJ 7HYnl@sc6YLir[V.M^ _FI[f'}IHήgm:a:E0 @ (5!EYF&U8fQ{;+=,nQEDƇm%P>}2%|rilaƚ2>2qfy ^!h.6ҴU ꚠ` IŎ;xΝ'''#---+S8vL$, p)'b+wdCZ)y2m~ TlZXLzqz&"wnRhT<1Nn܀np_#4OҮ1}*n-<4u2&lRQ=oF+P#aDz2'&g ˆR9]q Ojsg5EΖشa%7^G3P2))e;ɳޤ϶S<94m84˲0H_S. V}4Yo|Xb1) ZB3D*]z n! Tઙmi5T`1"EfTPF{zaE>("3Ю4S(r6?6:9?Č3/xB,YAj mJR̲}ݽo^>ЯNΩ˓:r^=?/sX#. _ŖKBsIHƅ"KՐ ,xLкS+m5nK&Eh//=/)dO ί~;@'IW(#Ȝt,ˬYwK(SUMuBׇXuUZ$.f:wY),hU+a[2a|ə>_S6˭h"h klgh$/4hI-qԡ=x3x!zFQ<O g)o9p*мW-ǴEͬPәA([x-l߹KWnUb\uk8uRԪYtɧN:E.oل-oN,6KB*YӃ. 1LyZY4`- hzgU0N̫^{6C^7es1 +0v=R;AUs+wGaZdtjJ|ĕ%k'5sXim^bCy;hIw/j撰.%asI(7/4K@|܁YuAJ*\4 nJI~, IDAT):Hs OZBi%\0*}i_=EoI!Z<-G2~#+'26* [ rW3`rQ4,F>#c*eQh._uT8}$P%abdAW% q\ˎ5MC2*"*C/Nt,Y^dͥb,T'N.d y 1$:SR~ Fp|e^2I)]}uV>;Pؼ]`6WKðg35(0/6r86dXNOSfIػa5vc~hݨx x $_3{MH'W'o֮A˯ѻ;qEzL#)Cee '̕eҚpV:c+ d h'ԇ)c҆88* 838yZ udct8%4Y'4Nւ}pekj/ uଵWh؁2a[3XL@k4.ၮw]̟?w~+&:wm.-jH;516Y`5c˪<! o׼.!Z7eKv1v8i`?Tˮ} Y6J%!]. Ȭၰ`VJi?|juܬ ΚNOm\tU2]Ī{IZsE1_{ۡ':@\ȜW> ëcӚ1d60_J:b~md6X8\_|ڽj. ӼK^G3A۔vfgbӏFΝ:$w^|gh3B!EE#}ꔑR䀬 B_L桏o ݀x:k>W,Xjg!|;hM22PMYr6_^fTNb ,d׭Тs>nϙN팹/_O[~]PkpFhP)k*uQ!H>r3!k/kH\1O2dHўN2K7(|/$ʟ,v\6+Ɇ [E/:ZVE*W[0oUU*ahSS(yv飜`N[}v]_3p_w ,!U08U\asK^GKD:] m8x"9 o7/ҬYШF $RyMSGהt$?SFdk/l7L+6j CuVXrLB _ujÖ*Z/maKIiV'XRሎzp8 ϑ-qF\y0izD/<~(QB6]GEYSӞZyQDp] z8tE'+ȓn؎mZ92؅mpRV'5k=nKsI :,`ʘKCG0L 'JrPG|64F\D/,? F9ZajQޖ@I\t8q6lˋE)>`UHI0ڍ".@~>_ݛ8Kš`>'F%!f sօ|]`h:-SY ,2<MJفAvzu}R@hB' Cw{{j$/P*" +Mo1Л^槆2H1h =2VBQ4x`;T]{Ъ)dĺ+jzwcaaULAI={}5x3y^sݸ|HOKu* RgN󠝣2RX3TvZ^$E_xqtk]BA0{PW\cqE$pJ9 ΅?* Q 02H% SɞA / rמLN^cؼy^Ff E4s:\diPaTys9-nLְ ʟYz(J HGp|)2܊*M3vz+#WQ˚Z[?\NBioaтhWO TtvS6Jo) $ʅKMIR V :`k^Qj+z MHCmŐA-Iq:2rwƑsU ̟jRQڟ2y cU!?@(_7-@7~ ]z2 v`#NJWw*JUZŪsUNf!:oWnIIm36#wk1zM5c_B[N[ tIL^$ F^LpH{3 efܱqsj'UdX$(n:WG^ӿ˰TV_>? PoҒ 9|vt^ljUh3H#t 5cR+ßgG AAS4 mHOt/2Wҽ[fG |w.mVqUeݫi5w֘aAJuJíɊ_xw>:kRˡpifHş/-ݐhY"&~@Gu@g8^s5~1<~0ӼNpF s(S.: uJ .ҩ0>a"p,Ȇ͗JxYHנ6Ss 7 lQC$p$ۗZ(8HRCmS-L&7"c)0!^@ 7YOAn֨7H*t_OǡJ/`_T]#`撰. %ampwI }%kC˨oʥCYQՖ@U  pO)@kIHYV*⪉N%)]7ġ޳rdLm^aq%;3@f̢4cLHJ#NCC"Vͧ\?>LKQ"}9Ñou սV৴,PUg~,{Ev$)F5p'.WcTƦĵ}r#pc\զ+܋f#V?W>,c&Y'00:+)S-4o< G_睋OgiD bɨ  TށTSJSJ".se-u=(ϡv&^,bap݇y6z?hgC~:QChH4j/=t gQզTǟ7\aӏe_2ڇ-sBy9lXNdh0! Ţw.YبBRCkj«0xxn6N iRםўļօWcϹwy. K`UcUF0Lp/~W:|8aحh+ј9{?K]Gl 0͟[@~nA*j%' NÎdzt%{ <{^WeBWt"u.* 0.T֙RQxP"e"YQb[c6 FG71 1 RcTw\>w ;5>l>ӵzKxΒJY^H?2.@AJM)=uR_Z #y_Gg7 k#PKv3$QQ_US J6hVM-S{}ȵwlbn%gI0Y ݻ# PDEXQ_T5YNKW%5˥2Xl0t^&# QyPf@S.rPAb!Ay`q>*>Rn_4h]hM]{҅c~ězЍNiV/3QX ֹ^ryҒt'Pi4)h[!$4'Ƚ&) "/w> /*v>ݾONx5WCǑ' rK䫲 c0{Ӱ >;yAz{}z GzL.޼L*Joǜ̫'Usb͛"ob;@&$]ZB#AuBpjY -V㰾LpL-Ш%g-m0V+*@^ćtI$HJ>AЃ7-| o%P!@j𑕵%,S~QgEM{ FH,iA%U.vol>tjמoNv:⃻ѸP=kGGׁtK4`ld(V{ǤxHa1jA~Uͭf %fߓ~YtChuI2/;l$TQ8yhڨ5R.Ze.fl-YfOix˚xivl'uY]#5%&{ܭѡp.<@[{lXq`TL} L|fvӐҁ 2 w.ǿ\K^F?|,0es‎wRBrCqjFh3@ WK'0:W]:̊~sjK|ZgK]ԎW)A*rҳMQPhtFPpi]q4o/P% 7b0K].v]ԣoJ+N9Ȣ"8qBmLH &ؒ; _fL؁SNVS `l@_P.҇tP{O1ߺqpc{ ۻ$b/,}$h|N<Ẑj_ _8յ@Խ{6i=/x~(kVTJoT{҈ˤ%Uy{ͯi"/]/`fPZ lb{Pmx 5W{:b o6 gY{^DgAi0r*qUmVJI(vzGe:]Ջ@EmQ^e̪}\!c:h}ܵc1p`͌6nE7S'84^TqVCQ mFJ4q@mI뭍6GmƓ?$]AcDR&7ET`G%q9&~ 9nCIX7U@v;4,ߺ ;GG7֗=t?p|-ڽ&l)*MA(Yqüoivƣ=xπ Wb{Xy g,:%zzf!r\)ӕ!Pu}imSjtZ^PЕF@&b7n8uaO$, 5ry S-E~!oe +f殸}Vj]DQrcZA Rnɰd$,sUϒ?bDfrw[環. 4KJӢ]BH@FEKHCT0VTدd@ͧ iTyS ^5U_@e+"Rvѫ~ѯ^E߅v8:]F:Y3ü]v 5iNw1K2UMPUVؠ.V*vYFLj"#Zx;q 8P\&6&-{XiOZӪ&*Uk*_*)s`:aqN=qL)ޏ=y_wideO5с~oݏKIMALl{nz7#j RicsO`{SPElX~ZB(D-zG{)d>HRw`g#y~mĬVa5W>{Wv8%wlVO NCX1 G\*ut!O7`/ȗ zEA ,ȴI76κ]xΪN'ub2]D"qg>| Ai ?ʢjT@k77jAH/͚Qi)S`JE VT㵯}_ʥ.^tCWs.O`wǝm̝=Y# Clx]{C$p,n-l\}a>ut^aX?чooTa"VK7ko厷uc[m fA{6}tyaM(%ګPXv'+Y }5Va{luPl8zcfZv/>Ŷۮ=zi̦3 `X~jD՚"U_K\=Q.λ9T/CP%uBx=.>W| @ϟƌE'bP:`VY cp$,]MGݬLo'@x_w;q=LNɘmY]iƏ<O> y $Y>b^T#4jy)^V<0mˢ,`N#pGTZYtYf @10 IDATzJxZҢ6>ŇlKNKt\w $a%N+rIYJ0uyJM' m%8kK[ڮyzygڔoNY]п) #vj4pI!s*QG ,Q ؙ> ˴L\fһ^6~j? 3SRH'yy"Lę4w '4c$!%<3@-p8aHK̞WA1zk փ+7Y| cAqi—ĴɌW 521sx-:鈸? sp8T0pX?+?../t[enǵʭݩauYׯZ.r^}:n9_-Yv ?ŋ1bP\qgsK 8G/UL2b\)Ω-ɨMWR "ңO,U(4HST/G ?n@^H2k?9Ǒ~pwavr']$uמi% yVzƎ^Eo:B6)NGNqLJM+.%A7ZV.G!B&<IB3pJB qJBG1YĢLT2u^Zb]@4v'\BSe>_#4B5:l(+*֟i%LVu/\x0WT4 ed.Дҗ݆ʺK .W4JV4Mԙ ]Bqh ^ؼ.6 ¨Z>&AR_}G\z%vJfRmLq~oBT(/Z!K}ӓ*jgzY*Yd9GD#`1b̠f@E[̛\{:+1h冬ؔƶQ~:w6p `Mh^Վ'94>Qۼ7# _݈e8ܼ m[ks9gGKyw\ӫdL=Z>2Mb,]6_6[6f?2xq'\aohק|fkKj=Y.2o&mhݺo5MjF]aDoȩc>S+]0hK: 2]K ނʓeEɊ$WST#t;{(ѥW 2A?1QDbm:^TdRɰ٨t?#pabko0fV⒄S)+DB;?AR4 YV=:nϰ`jh<64R(ڐ8dFgt7\WjNqJ$,iXԩ NOkiqTx|씌i"^ [)ͼll-("[ֱ_"kq1/:&4}.ERIMHl;݌Wޚ{mcag8l2r>ꨠSb[Ak]OYsL#NN=b-7&EwqNϊwMQĵ/`)Ƣ!TF~[;o $ޒyS/}2ȝ4~_2(x_c2&$} 7{Al{Mm ,b8,# jI!Ѿ-e`Re}1s~͂z.,GUuivv%[zOƵ$uҍ _&kJ#Txb2p(QL0hNiO@H(gΛ~ S _s5p,\EcE2UTd4m3ej V[[#*s]XbƘ6J:X9b7"\$~3MЍA4m[L1{h8ٺSpqqnp֙_owwN`7an$!}zZ;IiZ8ѹ)>Dh(T K*5M#w/-rE!O7BK6Ko$s K"ʕeiB8՟F4٨֧:HtFRV#F>zJ+/gPW~}]J*BjsYrU`N*;SnD$rRQQ 3t/Y\|?g)4%D7W]F0Md2V~ (_Ff⍫^B/4Gp}6{VC*5|EIQ|3 ʇ:郺/:+&xG,'ZqOޮ 4, z&`ccH b=)[oN"}MN>W$V}Vڒ%d6F,PxM(orq@޼}yVoKtpbVJCXg7ly}8e HWm._8Kh@kTS{7ҒKLt=TčpUd57Heprdx2pwY %z,!:f"=Rzf6㤵m`D12II"VCPR"?=/ ݋gNZh;]pN/'$% D/-+(=srRJ˳EpR B#? S}=kp/?Htvx^S)ҟuDzBxe{Of7/ ˘3w2aH,#l(*,2 \U<+Q~“05ϵ(MdExTA4Fd(;5.cEt(a/g1oediE .l-+q){S>ӲM/EAFE$ ᠺ6f͜*Q3 G77e|Ir"):YELn@jpđ4)F]AD/>|#eԖ.I~)OIgP i59yWmSQZToJS4< F*ydԣF,Skڴ%^QTﺢǦg[=z?yS0/)OM3ј^/7%p/ID*+/^)PweePeIz"f=VɍNHGO;Y|?ABǚRD_S5%^._jj*U wB0d#ֲ UpQM|L?^ X4+^QL/R)Gc{[Ŀ<?wa{qʋi"3j;v.۴ kA#wƝbq7a~q]iO2zF/ OHQ ?&} 1e|N_mEHeP.}E'CUDԣWԿUz2Ϻy O K?k}xaΖC_0ϼ#Lf!H+=FHG&EY;5)93Zm03wa@Kz,g!QrGn.%>%4Oz5,3bTya^4xB GrwEXi7ƚ+27"7]u򲛲2):Raj [1R;~w[>>U_ؾ2fxOJAJ']Rj8 +u$eo[WX4^{cqۣA澌/殒D~_8w4gaضpbB^*7mFnGgmԋ^¨\&>(ܺsO6<8h!<&?`p՗^UlUofL7mԵmߎ[Yl}/SVu\Fo⼛>~c":dQWyK2p?@+:gFgLl#eyB$zDYPEjo]EuAXrY~YYH)G|-$D5m qw0WQ8wƍYb-ಥ0PaёsʥVC`'06(.Hs/ٵe$R*YIt<J X`n"JЛJ [>\E  !?x_x٧@~5\Y`j A366"(X g;g2X *]WʋIh6qNAߋMa{Цπs4 #1bXaK 5(nTύp0ij4L ̤ 0d$AS.љ4SZsSEM"A~HMj2<˸F_ -ڠ#:pI'Q},Ydr_ SS"+gU38$DNXp!M^x|aZIp=!䳜yHt=݆ EtJ2ʒ%()-t -3}' A:dH_9}/uw0VI['˽ӟ/%;7WtOCe+*JkЯF:865x{*1C;WC}ߺ6ܼAfn?3Sim/܇8QXia8͞Cw+O~v9P2^(&Q*Q~2y6~_x2nSwr'o>s+2ZOƬ V˄dގm2,G O +L]<h?O΍#ٹ]U哬܈ݠSPS(݅E~c;bHl0E>8w|a~8;ꋦ={rplK xjHZTM(TmOi 2HuNAձSq.FRmFodzJtL%JWB/7鑪s^rz_R]<%HO(H\FOoؕ;=HGE/%Ԋ-Jʜ#e$=| s!$*=e`pp:fbV7]?eF< @]LJ9VoZneޏwAŏ:L=q>}=F]Pް\yn9< m7.Ow'9n8"\g_G܉U8XM^/<}cwKS'xj1| IDATp ȯ?|8m7U +}("tkeF9^XRã `s}c?%LR9ڳQBV%/z\Rz6&M.z&Q>/m7o# )Aһ>@8}̸*VϞsU mkNڟiJC7W|S"ȋSSlפW-.#d I7bҠO"~ |Vr4[001Fхi4]N9)G$[tpЯȮ aRT2IXr^ y<%I4 mqP2Z~+r+_l~lϝ-[yj4ۣiܕ],𓒍{LT 5q{Yd]H~FUP;rC8| \G$z||io2j0:&_!a'KFDZL nXϠצD2 D&DZH/q'tikn]+="=GiL!xٺ"ɬT˓x.ҟd*5/jA`xys[C9ji:[Ћ*`ؿ"vk_gh Wuhsx_Vv.6Cv;ok 3S&MKcW˓ag|W8 [IVK5\SYfĆ)wVx䛉[80L= ;~vjܞ.x< mxA?cB%?"~){̸J)i`0и(&sav+ -}݇KGN``S @aK?g$e44pUFm&L2tAiK4sy:5&Fj|M(TFk-ݍNழ3i^'?k\I~bÆb=ǚ'_UɣƔw)(dS'u*yH $K&"N1V8Nnag]Sfcn< 6vp$?ssJ%o).qDhpR>8آ/CrC::>QX.E0v(ג-3p nzNypOb,|io-7U'`|B!!3 e%̷&x_k7wBNl0ь̘qCGƇNTRGg; < h86쏃X7=P$nQ[*t9"lYYW'X6xE'LǸ$2lү>][vuy_p|pԻCw"p5!k$ 9n#0u*|V AW>NδI/x{*Fr !r/UT *u"LC5Aa*ᛮ%FjYHˍ4CU0( k|-:u!߬PUR(CD"w8^56 ObG#2cI{X7=l6=Ӷ4$9[9?gk+Dȉgyk08%q[һq$eD6 gß~2D4tÉ=~pWg&9>襋b5 qӛ pوYN2 #{e\:sBӯ4+ҫ^Qn*0b`O5ܐTGln:|9kOjXc^{m@m-)m#}Q{NATG7/ 'm}—ҩku<#y(S#i8F~aMp mnmXѦq]a|ݺ8kSY:aq# 1bH4^Y+SVǧsm-/N  t7&WoK3Aѣ(a9n}riG(T0;Ub4 o2w=2,sxG*`Y13a0~|N8 \l@m&֬|yra{mUcv5` +__a 8~ ~v kF?a=p.w &<2Beq˺t):X3LӨa2^iOC3t4l*kkqs0OIdU}4P[i)Ф5OUՆjqpr>=ڵMOJ3-.Z#_j,7 c-~:OWe~+3ӬFSb+/{؈]2FL]vM[4#@}lvgVqgPB_Yc\y+.He#=Mo NÓ 3qZhcnm˿, salML_\P\L#D +wqM>7Nܞfr8g8 ;(x ŁK_]ͨj%cu:Ħ;arz<7N8nB7-\N !4t뛱dhx:.‹78gL羁5ќگ);s_.=FƓsAֺV,(uu-a >|?88_6~j.W3} 1P(.=ƳcQ~zln0;NW]}8VU)"WÊ}qO#KP}}lJVPQT/ _eNHjEHj96hV}ˆ G#H'6g蜥0EO?_9VᨣƘK/ſu$McD%BĪM1dS>ڰ^cpN^|lԖ6a/c`XOxjd3ȟ…9a S^I;3N4ɘ-В+b}qG.ip΁l '{;֭MMiPeHiQw8Gʸ!iÖ삂T&&J)Z5ԉ66e[FI6E24ḢTg؍+8w6C*FG^UTrP*{q6#L((wirO ,IЉtq"UiPQH &:ˤ4_0- GSHJ)\JɼوJddN\< yN@Uo`vBՍBդZY4rĄ;t<#GƵ՞s?X#zu=qԠ[YcJWǝޣAcv 2, 1g=u"@qG*ȇ#˝X5H)U|u#g[rjTFۄX)=Ra<^ Cլs;K [o ,Â¥/g/u0 ]>%Tp]lĂbE؛|V x\҈eyY3 ㈇!o<ܸiԅT5)`jlܜ6745DlCtXV[ST;b8FP(Wh'&\#W82m>e̓OK'.s?s<}". nIIxGóAD~{t T\\ɒ!ki(KUFft.,a/6KPV%-TK#`X~ٛQu^IJa6%a R/ Yd02gH%l0]q5]Ue*yuO%Y,`6"hFOj{$a6Y0NQM7LjST{ HbaY(?`kJzC4V4z-(%XwK4S WROh+S4#Jԫ9zzlb_B; Ux'i_-Om~#(M?9ixyOX𨧥GfVF1w3Qq{PWjHqvĘ|"qv@ !Fd(&:TkLMz^;`l߬ti+%7yd+Y{_EgQZۃw>]?BSpK]9bՌ5f3~qon>M;.<(ؿ3کn#qJ}_봹5c&xm8Z-L┕Wˣ3/&69OE7swǜ9F}]hjx!/);O68|Agt*d sj(%`l/~m"LuzǗx/L,U2ϲTGMpzTuGmrD4TNhթh>T^9w][k?z˧?]a~_Nȑ#1xPo:j]\! kR]0%^z~/tUߪSz#a.mˈ `aE2?A2?Q(S(|^dΰ3)(863hHN`覼XlfyvZ*"G X~(]BU7o +`,?aJES-e|8ґo='b O##P!M׏0* [ɘU PJ 2uNs򛃄ܗF{UEGZaUTsQ:OVR  P1Kxx~|JBs]LKu)(?F%!J1*"*&=t[K+ ċ#$>ųi |}!^Ѣ؁#›В#ep(aoL:ԓ eݮ'qdW8'[ui G$ Kւjj}A Q6fbh= :nsW`5h )r[r뼹K8u:nKGY9Dr2SWҨ8u7>Kx \{cvױR cTq=W(d6Bώ]xm5Iu|h* i'p4yezߟfҮ}:&z_Z҄Kr͟*5JJx޽㨴'UU`#Ң5U) ԊjmNza/J 9uZڡrtv-n;ԸONP#S,EU1-%IݴGʽ2G.ˊP񚢍6 <|'D脌d%83Ƣ7i!,tWi<٭ B4)+}Y~#P@j~]讒]gu+r\BS0dJ?$ˆpWRH*q2RC Y_N#2 #dK$[CRr)d8߉-#QkdXȨὰSYkhDW1q(A̺Y'$) +Ъ;-Z??oBDk}u!0u7@͜VbQH8_FSFxhh_ a镟܂~+ n^XCV~KzǩK}:Q/ewfHuC{c2H]F8ƜX>(_ 4=sМdN{to"sWJӁ_⧳|"@ O'p#k̡qܳ7ZXt8{TKҗрA̙1}GB8OWh?]a֧)ϊO9d|j[8e+D;w%z6m N _-lօYuDGHYWhj?E4M #VSAH*_`K#NiTcr7.dv?ws=j/22O#9\R=J%yߚ9ocyQJ̄ 0q]qw#WʼnrI iYQ5'+Y'ƬFRR+.++ D`e+xE0+;%+<,p IFɠ'aݣ_8*2W]Tq x nʏH, e/K [5.-Bfeu%),}+ɒz6q*vf!*gTV H^Džw _"rK 33\#]խ^~9z<] 2ޝ/SQL 3 o&$璄fa{ALBc&d01߆ur CG!L&ГZ->Ez_&Xґ1N+")v#<)K)B%Dό֦TY' =MlgΞC`#ղLXek^:G?mUi m/A|Z*nF;6FqǍ[c,:FV΃x"3`~ВK,|Zke]ȭ E?AJJ:╚`CgkO ~!Ō. ׺2]5XE\N,0ϛd~I`=u'3j͓N:b.sM17лGXT!pxu.*%̝4,9Z,R\Jf8,lLGZ n%J,kP2)87|҇Hu^&G*`殢 K呡*^ىy;m\ԍPa}%PdT s% hw#OINU(UHB@(oڐ4$ps7a:!fCV)^)$z #Gyg%F_xn$qnįd)r作KK*YM0_G0287~}vg#;^z> T1?mpN'V]p+{}{<{iU9nus5'`3?ݎ7Sĕ7?@1o18[3,_s.q0a,ŧ9eA*Epݫ2٧3Gf%ÓӖ2z&/KNjh=ct CUƩ R%$v'EH*ӯY®~Nonf4<UgptE]n:אhԤ Vz֢/Hs8_y^ZT?֩B]D(I'՚[ᛃpѦShVSt3n yp)_ R _}Gu9Z\f V\˗cɒ%809dbkwGL%d爧FZmpjCCitWg6X52ꊕ`{Ga k#J? c7HFS0nU؉7 :A峍T9\صeT1l4%aW2 u~e<ӧFe Ϫ5k1hoQPwҀUȒU+в{/}Ѫ[/~b9j޲ˆE&܂>3.ЗKKYKl΁8o4>s颗yX1hy|G=R+> t5ڼ>t;};pSo,zh|8%AΕ$_Ykcn%~e6„vV&0Y}rNuW5kU-7熬ѻC2/ +<3kbqԮ[#{x}ki0gjzaT;=ږFk:5/TUPجVеӐ9bE9'}OԬBuܟڟj(Vv+ S_\lPNNm ՌvU&1RpRX#տeozG}xGnB۶mQ͵d[ٜ40 .c1[JGk., 4c3I|:nnMC4 J,Ldf|՛%_ܧ#|Hq)z,ʛgZꢈOq#[G mUVO{kt6$A  J#^ s2wHo. &/ 3̙f5pMt)0xxQ@L9O^:[D*Yp> z~$Ō|12s$ɄҸBPFYEߣH:VQFpwO(XQ2'``e5SVXv|xOL!u R6JihNICt ֽ ֤i}qޟWi Sue-ӮtTlH׏Tii՚0T[~:-\C?bֲ7߂GA1v =2DPD'_T2f Ytj>|`a6ݑ:Kj6B bX:ECM9a$=ݰ'섂ћ`?k9`覼Y͑M; :עtP]F+f1K,M}e!\9mҨ7:393>=pwNFǯqݏa7]k3.>||׀a߽|ߛv5qNwfy>=7[/É\.qr㜃/]TtqG#JE*IJ 3 *jB+KMb7?++kFi@tl+0?Z查*cw>ֵ\ ƽ>,rs } .|VL LTRek|exI.74Sq2u`b#;8،t ;->fukRsF-L'1˸%?U |є'v>]x" tf<2 S4:AGSYf)pzԴSkŎ~=E3vs^A5So*/א 9?ݺuo~{E};Ez!߅T[fCjO~‰WqrXqtj4)σ=8eΈU0DB'XQ]F??c&~|xxa),8א;)gp6հ$lB\Xٻێ5bT -g`*)t޿mwnn w<8À.ģ˧FW =;xݰ/=!A]>/ O q{L <:s.Lh1c?MGli?nkcՏ'Aef:5n]EF(F}/^ M%GtX{YLKWLJܼԊX@<~ߴ5GpZ|q -CK&oJn=n)lR zÖӇوpNSϔGV)M_E_Si wr2h ř#bc%a^F/Ry--M8O 3XάT'&ٖ S:NJ/MheVN#9f`9Q j:^ FqږGOiUy?sTu]kȁρӎ?2v93NMZ Ph4veDQel5џK]8Gc/IN ΃5 BQM'ZMdp{C-W?Xč}7hB4)s6ʹt!iiDg, :^aQ%D!$XN珶AM$.N_BESz8Q\=in5Bfzr"Xrp$"_YKp[öBzRy]FF _z5vH]ǩ~~#S5¦ͯQT#/Qא 9А-ύ=~ \SՒ{bղԑ8RQcwoȷ3U6b pu IDjHP݈8 >{Vi =13.g(52 +1˧;\}Nߐ nʤ-xL:I dF1U8[Ek;Z+d:yE!*I+j`0Ha cpw;I FƯ`rfɯ꾲hIS!P9ёYqH#z3z/.Dscb)_{3^'t ƽw&'[=uM!?7*HrzSEŜUkj2LlXqMgBIO DR;Uph7cƔw4׆˥%sٮjw1e9 \`eJ`(!x8)V0]/%f(7%aa&JpM% ˎFsXTω`a1cn}OqZX/(2P=Kj9կ2`cU_ʟwkQTZ- SJ U9kpiȁhȁ9Є[8fr\<&h;%mOb_dh"z üT!dy+v"!~๣Qߞq7α`Ě\ L,2hv1^W~?F+ʮ@*4~c AMi˨3Vh mFp~_AiU`.{˟igXѾy9vޓ8w1E3Ey`?б׳}.6&+urk`pU!5J,-#+rEÊmMea*LUɕL]~+0r$2LtBo`.;U<˹ nz]m6^뗠 >k>D k=B~.e~OkUj$5G(FR STB#Χ|HxFAZӝrg6hȁhȁ=tmwމSN>|{kvi q\ku=pRstw Fnř J*$Gpr-drD6tٷ) ~H߿2 w2Fq(\I3sCC*{>2L*ጋO 4+VSB[ی勗aSE:W/Cm*Gۏp~ۡ+آiblF,bM3 ڲv3.UjъU-d1FӱR 7IE* [+ xTOİS/Ҫrk7|Ż|BSX-D#Lų"r|~QqXD+qJΙCKmn[j-^\U֢Zkr-0Oњ1Z05,6L3c\C4@C]߾u, IDATx!~|OFO4餾67#Sԉp~p_xEr;<:Ut/`\{ {w| z> <~cm_ۣa*٦GVHMu_V(n6^aлNQ'U2T8VNeMв0z1[8+?0Gm-ڣƫ ]` ^OCkF@IRp ʍfM֎h٦0T/8 h9[q9oe@mYy4#yYNF[~m^! WzXVpoll4;pѾ)oq]S4H ip 9А 9А9o} +aOxyD(KJVIRr'Ġ4@$$7h$#GtXCn"[x t-o|ZiJܙ!G8\i @N9|'⅗_ƣ>>:vUc[~)%.;&L=^btڅ~ɭ.)ΌZsm1<޿#"uؙO| rD uEU Lh#gM9+O>ty1Ϥ3j: Y$EK#uWZ=hJ˕2)k@?%XlWEsoE_"# zkV#{0/of5:Ekv)Tf#psK`D:ūKol͖T?V$A_}G*{kZMLKk.1 x! 0]l,ǔU[n{-6 p~ w!\ (D4iرc1rH\up24J6 6| /Y˲cf!Ubsar0jesyXؔ@'~.]^} koq7G@t.wUk7bƭϣ-2P )!Km_ ,Z l2ߥ|Ĺ0L~1Lf(ʗPű|TU&:o6lCuf`KDb<Zr.yM~r-`dp 3L<@ع@Z6/<^KƙGt" D4h \u58S CFˆ ;T`J|4;.ƭr'mti*|0+Hp_@6Wz `5pZu? l*r,s%(B"4lE`RmPy]2a<,JeW`Q~@*b1^cެB2 1O[]'Hky`/,T̝a`Hm3 2 zȝ{?q/~bszk}".Ti9ϰctT4*[˜P Xs+':YyH3Yj;oXX`ல%{FH" |w j \z[1ѶIѯFy1= 7D%!hMUL/XOO~!$ pBG@tt>1}+tmrj:%zP%=!yV٢DfJ$(HuCd#կ)Z8>5@$W ւ<)8Xj8jfZgH)H~\(e`ڧ qHnֶ5:h EKS}}9ݾ imQ8,l/]GRꀨw j{rPV"z &ץE#ṾR1J.6kѱ3r;P vMA嫱|Q,á~ӟxpeR9y=Az k# `*^V8/58r"i. " DWe.o_g=tm`NЧm[nrd )dF9tˎ||L/Sh]q#}4{#GЍoYH@GBi=]g#PQ8@SK#Pik8S9Ah)%:*}T ! /m3@,BHNGK*zB&X VPʠ5eKڹNkKbT멚X9"W/Ǖ wjh0A~XMA|S# TH~KaÆ /T?nG@wy c II $:P2< 7>VU4!;A!cxIRgSA:Ye!#0,@iAr%[{fMR՝c@֨Q>y6}MzWwߟ,T mw2C}c}IIHmuD 4w ,ޝǠޞe+ߊj` x2ܹ@+ L, pcH" t4]U۶mщ/;#kA$f]eU.†,0Ɓcy  T1_3 U0V:ⳟ]n $?a%t_.qZkj 8 8ŭ&׬ۊ+BIqjB ˔4󴼋I٫?j?h>g$=ﮟo/E*Ы^൫|uNYu, ^]ѡ8ը h*c..mm(j`֩=}4΂r[YmqY+<$Kp*VfUm܌ p8 p(D4i VKs?s~_ ֵUA&uŽ| 47[v_` ,2LP"t(a`4fIf<t Q xЄE'<8(&sF tpcOA5d UN(-xx|$ʞީ3;wA庵gkXXooZybevouM[D&땟ĥ`c",7w3t3.|Vc\ v+,J&%M3CEL(Kp+D,/FropVsR" 1@H7(_obwΙ<ӿ&DUQ lxOJ (:\oU4`F,^gJ4c!rYEs̚Kcb&zpv%/|]qw,6cYH?#21FX]Cu FGzC$>_%7BލP]͗`Gu>;_2n7, OWrUD'OJv ?UdUq9HZVY-RW-1`U& IJ.%QE͸kEz}ٱ8> V-mHO7ZBj@w&-+j ܬ.W= })7)W" D4i ~rxŸ <{6L)'}f( Io  ҡW1~TGRϲ:K2ʓlcF'뮄Ϯ[1 K抗J3TH3mG"6̠V@8YjOy)@l\'% ݺ kW!g1l_17g̪pgUo=LgU'h(?y9` RԘ*3Lkz?ke(C>1/:Wت !Hf8J;LE>>mDe9BIĪLr<խ,"dYhSQY- &D2dI6?va x8HnIROÄQ:oy |2)+| 10FMOui?"ݝ<[;'@T&K i `Xyt⬉Δe3 bt L>G;i>Ա%x[$14 FB;@%(-:jlY TbTv A t~W&}Xen[ VD pZcMvA ;E4" D4iS%&&⎻أB)h/7kBw$ i6;˭-,$0B&,nLEqu %(2‹F1@j҃Zj>n[Z[EqEV|eׁ0V’Myqm/S@Zkm4FD; xR0g}')?hj'ѱIKVýѳ'rW,GΜyQ–FND*Asʉ z7YUc,:5@ w$X2ҏjrMAv@@ڥknv)0?#,RbAf>d]coa.H@yXWP 3 iNhvsXO/XW7l:8r!:E4i j ߪ`k[ԃI造MD,rSmO2`@APF@̉^5UnVHCZR \)`i[+L *`'U H[!Nb$ x;~@xV<r*)G --`2"!`b|Z i[yBxY'D~B cV h̭)ԭH~={1&*@Wj1YgJrqzi,,w`$8. lN,kJi-Ƹ, 9Y[rG!@H" P?7܈޽z}wC6&T..7&_Zc5uVYI%0ϲ!C}cDR׻R>V"VyxVg]Z1Yx>`wpsnx8Iu 1=d Q˲G(SuJ[1XlBTQ Z:n;톹5c/`fa8(lImN1XB(Ccr-zpI'K>]ƒkF,O`Y`]@ k/A>ȭAzFLy.nJlb&@^T$Vƃ{ Ι2rŵ/2XM=\r5[38T勞FuS(3>i*l&ҍ\@,z)@H?S D@;\أ: o:-O?IV;qZoMAN2((8@,ɒGj V Ԙf;c @Zc{Oǂx䱋P"h:$?9\b}B:<J[P>?*wsYcwKwbux85MP/5 R ~ -?: ~HsNRf}Ij] ol@-9j}++ e5[Wƀn(Ng>8lZ6Jxf6MA՚Wpaw2t1[1^;f 8KuXi!u,]r5^ 8 S~6.ߏ?z4@NDRV%.緯 =hG` uHr*&\!` BX;Vȶ D*G.N1@H?e D@;^o _G2sAֿ5󫀢ejslm@٩LYcd``E'@%l:jCkޢΤHoP>D`"a Zw#aSQ`%7ȨNbGby H B* WmPֶE|0M4j7s~?Q7<TgEx{Y:& +,𛛖N]#oqw5ӈ|."5Vò` X\[Pg`1iӹ_m/CIƚ& p؞]AwNӮu"3ar_!wgmlI;V|/PBLgVGL.o~*'"!zUon-VXicQZB^>e˺'2-i ^ |{|8I) " D4@te /8pi3>DBVp@0 d MƬDg"`s5㞟? w 5XEs'K^PD`7g wP!mH_|-ݸЍk*$knAЫP6)u0nɟ+%ؖJYsD>wуZWS-WǨ3eW\?5$Ri`ʕ3P6^l})$7)M0SPn@ţ)Ӿ_qhX` ZZ2>JljQ/ݔAA|rX/_7mM^"`VtH" /5S}cO \ +E8C2LFE! ËVK'HOi]bj{ SErHݍ/\5jO2:79B%QrƚkV]E8%tӻ+8`2 g؃z-4OCUa"ï޸w9f$ҪKYmA[$l]OfLc엁7{˖a@b)pǐhzꕘިDp;A.OgF܇O2Eo8׷ %DvrJ,anu5WӍ`X=^l7cz[Kq“'ZXz0Cu/ `&`r`z. " D4h z۷/{=>W ޗ[RFoM\G֮&4e=&1cQNsα3~c/;S f˂k.[θ ?g}vo ٧z;]>m DB,t.W@h.KZh媠s+M7߆c:qñNfCy8 ߦn|_\k:"1V;C,1, qFw ғ j D#`\t$tW,vax'NUBZH zVwwR:F{":99ؘHo_xֺR'ź"l^z+]NS"knt<98ࡕأ+ƤќEZ=Y.47\E|#/u`pig+ɕH4Pk&AIc.toSVu\" D4@tk : w?;n,i?>IyMf1SZ!'va,mtn=j883GONE_~[۾K~Bb2R:bP(~)|Mn&yC `bdT{,h|t]HN1]ƞDPHpmJRZ5m?8/or| ^[*Y Yv`r)N 6HD:-N <Hkfc& Y-HAaU&O@JXN*pmxe{Ws|O0C&9vcP84HzԔr!icѳOkks):נ^} 4xV>3 Vdٴrg*z8ޟ<9dsip j?c/^Vl)0ISGfaS&ߘX$܌+@h)4+*l 5\X' 0[DM!@~g;Æ yqUWbqpژ!H&=NNPLL%2@z <-HN2m&E oOYZvnjOfu֎|Lm nB>O(J?;EA^nDN*io4`uWqU(? LUڀ #!Զ#g/?n2EnPQe_os36ep?ܱ)|` _X!  Ғ 1ӇT] bjae yGY] mQzܼ] |.po`wi[;m4޸)ni/fl_Bp7]t+y\}2x0:5KEIB ƽ{IDnG ^ت|H+mtO44_z#ZAG9D%06 9jFwhɫ{9ౡ{ 0KxkA6~8PF +>8kCy&4Ų: \ Hͺ-/;np.Æ*[AGڊBH? D@u߀N>ioU8_퉌tU! ^BnrV̓\cZ]b]LPUxuI(.x?Ɛĵjiush;~ 7@ΚĝRQg[-2r肾$.Vtݔ/~9x"W.ZbZ঴ng-dlߑԴYyZ%_ ﻋxe/i'͛0-~|T: Cmg|sxs00sI~_ԚUYm ,N͂bK!{ӀhX9-~S_OT6c9;hW]-β|L TnYż1p{HѺ7.ԹhݣCV~>2v2ax Gȇ.CM)cņ#Tv촉8.=θ<hxvK 4/(0]U=MR NmtkiDopLL_X~XʍOM[nŌ3pB|5>jx[1}+gzF2KK](9k;G5c~xڷoW_˾T^}%2r2z9xw4m5d=Bx 'yC` ROWm^zSu$Ä?_#GBn?԰+ 5C@ܧQj>p!*ƀ)D*c#MPұ>!١`\=a!4$3в(^.2/BԋQf)RǛ曈YV2O7`,U 'CKx~4/\P hL;{*b >ys.ڝ:*`HIBNΈ߹l0i-:/ <5nhϲ;6YY0kfGtҞc/mn#x.o= Y줩[V,ڏ]w ][Qx'Ј/qxg?}!\qh4u\$(As/xSWRE<fc_4҉j=E:Ͳ0->Z=۰Ji&6 0A["Te~d()˹Ƶ$`WԳ/hvXŵ3N`_ SQJ[}J_x>/]Ͼ5KoE/ѧ_ 2C rnG?Gͱվ.QEEijc>l6s۰aFB\qav IDATOPO_w z)ti #zu{{y/驶ך~,H )WVV)ٷ?=x/{! O>kb\׶5$q.riI_<JjρFܿ3dX$$MgP]؆:U8TTn,w!*,DR`r`m? Y=v.e!zNp9&y|.-IKjކeq `Yw? w§qAwC8Q͐IQ`iZ :Tt_:f軻׹w#Sxg(^zEt7vB}pJNؗM/Gm'E4~ˏ&hr=f Ə;6bt rsslZrut%eiXЭ~w]}eyy=a>C<MgVг n 7A XHr\VE _,4>e9g1zcQF٠ ~NͶp٧q6 mTZHEtEi!.ZK Cu)?Fb3X?^\Q )e|8*GVd7h*gx--|!n}S kڗ xQ/v.E"l_'p9~/>߽#]q,x֩SD0j9*|SXO.'CƏBfR਱,yx]7/;>ƋENݑ߲-7mY|-~Ly U zHqeܩmWr9n7mz 1W -\vܩ+g-E}\n/ԑ$=O"uʵ_M3yr!YXBTTn~*qꪒ`eTYs(%Xxs|Sѝ1 0p XHT" (**-ފ;Mt@1ߥ P+˷e`wk wЋPz/=iҗñKP ;h@:wPO}&Mz6>cdgemh,mnF}2xLZOa2p2Yj#܈%c"t{! Ѡ$~9b4ͼog o\a/YK@*T\w?KNxrxɊˏ݅=̲ukb".Um'vM'*عB,\nXadwJ`.CΥjO<3ɃCɸp)CcW#Vix+},f3<[glqmUC.ݏƝQOAqAvqa)4JnI37$><l=AП+ZӫdPߞ_B* yc 0f&gW~e$% / ,]ދyG#p)as45~w%Dܘ?l0o8ȥ)Xֶ>(9,~ifDK e1_bgغel|m|-e!++[i kmڴ 0Afe7_ D 5=Y| 7BKpV⺉Nkdie*rЎp  ,.{ ˠъ"J׺Q*A^՛`yt(zs4ƿz'qg6M1pX+t&l[ aŜMh56̞E 7t<:v}M*>SD0*zTuv#]ϗڝnӷ?^)#V6~Y=Bu㯶#yt GD#V6z2kgZ;ƂήњV_`,ܝk1'Ju=:xoTXuO6}:Xn!zEj+m*KÒy_ogS_,19?:}]Qd^Hߪ-UtwsOҜ/п31;>fxQ(O,䇛N4Lv1-1@LZ|^\*W-Cr܈B m,EV ; s * :lw?LYܱx{01?ntV!wu7a mkp y|I~~R+]]2~̴hw/OEOD1̣~= egGĖǭg]}N1[wR7˷r#Rz]?فIUjinBV?wr⨼g&_F]֚߯ oWh-q5Fџ{1uιo]x-rYk-R\ï dA3j0_OKkuQ{೙x3m܌=#(]>n. ?? hd.٥O֕>inXoҗw5#fݐx:A[Nq6 !PXp&MQ]Zb o{Y͜nXSBs@묝:'u v%]%ABuj2Ԙ1u4;JԽ=Ee UaCGBB}k v<"d܊Pv%1Z^w]f,ƙsnE%ehTekD!9pʹNc 3x_ w8폣~]y4L=2YIw.Z+s}ІMF9"^nQHɨd*j Y)uoR+)Ǘ, Fo&Sޥt4euJKpZ ex|~`-֝M;4DXyMtjwqd~{?еF ޒƬ˯9wgvÛ;VYm}5ZC>ID2p攓pYhE])C^6|[AV^m7S[qqy3ӽwqqAqd"x-'l.ٺ*.U/Q#3wӸ,NOn7̗+/Gٜ۹nʫ7~ !w`7X";%9|q-~˛ ڄ%R@Kl 5~^K/Z+@m,ۧxg|%]}f-{\ L HlEAy Ҋ<'1Us&KOR~p4/__yw`ZʘcU Ȳ\ A'/$k۠kiIl N'd!>mpzzB0\ rொb\:)'v|11؝/]}#wg2{`U_]jB?=[ƳHOG=9o7~ 4k((4x/G}v6~6-Fx2oxqRNca;+h;&'@kd6^+*΃FJnP$6hVvr:8~)_} pV~NM*Z¹AiiӻӹEyae4 M޷:X5Oe-1Ak5+oM]<Ѩ. (Y3,]rPUs#_|]Mvt/'<<4ͤf_۷Y2rܛ{~}Q6^<9 W\W|rM׈eIl?+3Sz%K+gAQh:V77O1>!/eW: Pf|L 3S7BjѝYxŧqW|v0HivÅO _`Ps\8n )J5 ,.:D zZBXLvp\ruFN͒U-kJۘ^s,-aJhkstoi@1OpsѠ;VQ0_` ( @}4::}i-h0_d.Kl+mn^?Ѳ[u4~sq{@toF~HHJEj#SU4 O$5@oRfIVv_ԁwfʼ*B\gZ ew';/L-0oCI4ڛҊ+qjts&ѭ\l_'/~d֊kMX> zNˠ^kz 2[qf!.7Nnv~@Aʫ֡ږoǍ5$j}-i|9q;=«b>q Ѫ=:%B1NپMH/]qϾ*GzkĄ՘-;^CqG+:F#Ӡۭa͸KL.KC۟~PY^Hע(|HAy Hrʿ| ?Dla.f`RwpSL7;YeqxLFIC^7{˛OFڅ]q;<W"ܞ<%=  (8}RXm4k#jc2..L!"dyT,z N` /ڿZR<2w28 0-Rta_u9b}I5x ˭+=`PխNAW}g46}f(7/o 4VIlcM$ucP j_U?./ 2[bĻ`^ =O3*'@FQhס?Omk.Byz5s'⾛1@E ﯇ M9Ys-6F=s0_}$vdV'^A{"Z%/F;׃Zx2%Q_a'*3V ENt^WKpfn޳QQ `Vibr_4$ROcV.ЍȆcihdjk@SΛ7vÄ . ȳXt(\ ^n7\UB۞/7w_ꎩtwP;b qefMCoW7YFFaAwcEqy 5(_v_LRAְ'N4 .BU ! @oXXq}RXlM-,;(TnZi(Æ2,6mKC9xΧ߃٦.V<~5! twӫd$zBNiX59Ll,AŠ! 0x5&w 9 [eS%wv\Gúӏma݊6*OK_4g6 z!7ep<6xOtzbR'' Grăeרv꾘^ e0EI#!JnrF  wc\~?y\ַskܵ +n"Knkjq?k㲒Cnld`pmC}Hs]d7uZ9DA+_CA VF|YJK,>~+Z 'p7F/D}Eh !5iOOŊ[SHnʬIv[]|;WYw J}nt":pIm=Ubb[K|.[$dܱ3?Pɭk˾z[? m톛.( M&;&-x KpYqYk Ѡ 4lklFeڤп쮘Ѕx. {N? S̝pڔdq8|nxv>ػ뇯[÷a\XCY񅴬F5\[%bd\GUo]w-Xp̛=ZbI0GnCF|ܸ0=czYy:Z}.ž`Uy`([DalmmPs%O*hu Jh^Cu`[L 0]#{;,^Q\AsMcܫ+zEϧEs聗{}>,VՋI]@Z9J+HBH۠`ZK+`uXeH$'6>/^bRx4e3+:5n~F ⠃2i "_}wl@`'s:nNM+&Ǵκg,;1S\w@ÛcVf"1\Okq⍇VuU; IDAT4NMu0D!;;i/Na HfTǶ)5ZjQHֲQ~5i3֊Zbw~Y&n+ZIeIj( =oE3^GCQ]D;G% }"OB̢;g;|MۄB~-y;^¦>1Whc oSyNUug4kE3Fl[_4an'(GmDӠq3ZOШQHMi 0W}4Z.FR׋P=n nƒ(u~+G8fQFD4 ki<8PrTݴ8׈G=(ߴϚ8}F ^"#V|U*y=61m"];lrKB원b֡,tQ hoZ5ɮZ kpYk -HI_]'F'oK& r4*&`5 slI-`mM{@VqB!Y#'o^.-ѽbѽRh۱!h -md5crc%iQu!ǝю`86T}$J >>~.Sq}~ 6[802AMD}wc80l)hYPiNoaPߩ GUg37qsZTAڠ-m5b2h#FP {|.eސ'SO~jъk;]P{\>d$!gb;Ik4U؏"(Ng8?%myțĤֵ[ `7*t5 y<8,^x[#sy[rmo`5"o?_Z ۂ혧s6f`O?xj'L[PfiŽV0Rqt_=BTPir^$,o>+7<.r]y8su`\Q&,UP (`:hONF iMnyR>{#5Z޸%$t# hnV-ɮ0m;CGQMh/`-:SMj~fo&1G'AjvkmMǩ>hIU֦VFko?C.ee-[ȹ K3qhx7 YެMiVX*Y%)pa42:6C];P騥 L &F&TAQ!689Xt؆pqe:_0k7\6 4]ӿLAc)-Re3ƭ9s bԓϣ?rdApYh;«:)e7\).?\u@fť2-iXWK  -+x)w &1;V-Z -ݏq6mm7>3[r M,DaD_@4:> '#e"ոyS<Ps6><7[9g6|s4 љe-+C'݁K D?E& Ė731#Wcݳ>AO!N{[˛дBSՀ'~2/Wa|b` VK l0qIZ]AN<-Kt?# $~^[ G|Hj‡bgq<m~!wnA~={Bu mc}ܑ]- y Em4Rly2o[z.WT hWb <2ߘˑ̗Cw-3]{xzQzy-|Wނ6Ҳ~4U9`ΟV:MI%kj\{6޽J7,ABiKpW\ g܏Fc-ȒqYipq#O#ū>S^ZXyB9IJj& q~X, vbT1Nժ]58A>-梖$ >|s,q Fel^" [EŦ]~fU+ZwY,Z96WÊSXI7Pjibu$˅DsNQL ue#֬ jjE+e黎4AA\剟+,P,UyK}~'p>E%(.Kwam}>]sP\Ku"f$z9f?7cqƹ~wӢ_hC 뗧 >­AB W׶Nt "5ʊfi }>O|D=):xaι0 9 V,b”ŀugbT$fod!m6{FnCӃ9; :nWd*gAR9. h2_akC0h֞!jJpBKq DEufcWER}(5U%DEYa ەH0&e `Ӻ%e2%U0^-2:us2  W x z7H=}ܑNիW#%9ņ J Rx@T%s#qnCP?Ksu6{8Nll UT׸,rz}btyDĘo oߏVQ/ө@7\l'S5ߠ+t@Sbb`VCugۚܘK_qV.ș]/AO}),=iɃ0d.ܨF֢*KïKitn׌ J|sV>/sikT˖GTYZb5[@*3Oq0)D=` TYY_͜]҉!>@7q8EM|unnO>4F>E=tA➮l*^Ϥv'4[7=⪫dS<҄iڌȹ 룏|})1x$ X`!1eT4~x)!5 -+ON|2zU%ĞݤHqòN 3_F + խ!|^ƉOi3c;DM x$Tew>^)K}~I߁MW}X@ќYPGXtiN{ѹa|~Py+Ri_ٞB2դj*1rա*&ⲳ;7t X%\^{ OEzt #ţHO60d]/ :>[1}}i8=: ? :-ijv* RQ.jvsЩnx/7^*n(υ@w C 4cXS=_fkxh/A!B\*cUoKĪ%(݀~ڱZkcwk[Å^B+S7ͶU)cM%9wr El[xw1s.ԀpZu}6 e k`5T:cnVz_^Hiw5.rY8bJVaܼ,3oZj pj+ۚL.$PY]=t ̉4MUg^&?;3Cf(cضܢ-J\ _M[*,[o/*k&)lfcDW4z=Z$[32,˭zp$9pQ-|@XXzaY!:ُ((L5s@\zCG\Fܢ ~\=kɕj+†f` la’0+_hIUIt^5чuB D43VNaf &'"Z );"C{E`&n{*=qB?k M#s@p5` p4VqIښj Eںj,&D$X`c[je7e-fUNHJ.\^f^fP]Rl/`뿢HIT/}ŝAH_g 6`*ȰbboKT,t1ӹ 2<=#|Df3%/APvxULsw7Åx- g/1R5|I}$X>Tב~g9c3&'+ ;OOE\6fMhv) Aj@[lw`Ysv`VUV`.{p`%`KX/e\u ChVWªX}@,J#,Z itgwq̔AFi[a&]녕q;NӃ~m\wUsXWѲ;|)nDn)+j7/`(ҙ"h)g](KSVbMMt=uyüqR$çQLi\A-aQ0D*e}<GHWqLɡ~I}ow<~\˛h.EYRf+ĦŸQOm#,* 0SЮ6Gdv&+j\#4HWϭKLRׇ%,?DUs ;lL, (OMt~^z!tߣH0)@_bJ~I+#yG 7@RF 8kFZܫEsagcӓЬ wvC<4iii  KFbͽé/0v }(G͜L2 }"_r,3i"\N|9 e4nwr$?V޳߸OLd]Nŝ@,U  C >ZBt$掠կA% *y@<U[d_\Iy{ {+2A]̪z+OojɘN(Suu.H )zfsLr;beXȺ"' vA^Gg"K/K W\ҕz(!Y[LW0 zyu5J=@@{&5h v[ GOGQcZxEYDbXq^uw,feԏyivmEg'O]g3&9 gd$auiJ0Ht=2*T:RWGʡPS f_򷊐uIGP DSZVY߽(o'%),,kYi@l3P> PBYbn"@)cQj<<]U._RK Y^j;:NF+77^|Nw*aFش>iRz"u5Vdw8@= g>KP>ӋJp,[dPj]e˗[5OjWJ|nWyvE[m oR&5u0GJ1}rpdo˾X^:j;6WOt9/}7ky7[aT}|&,Ë!79 17F43,4cXyNO(hMjޒW VױH<Ҭ B o4{j%]Xy\֢5: ۠o\O$jg0 T0oNHkOŵ.t-DsV+[7y[#nuq^vRoah%zD %Ql YnHd=l=_:rHHdre!5}s#q{B?no7B?FVRॲx!؜_bߊhCu/n" Ykg\{Gͺ@~IZKNke IAii):A$529J<ն`J]w]݇}^WU.G2́Nuw,/t,\YOfe<9=i]`؀;]v^!A~xUQ}n 5j wj@/бrgHWU7AsM;/ Ͽ-o{"{3Fk3gpn^y5rOz*pi+cA.p?5zum+QX;^؊E!?fٛ8L_e 0#ߎ 7UNJGtӐ{BȒP?U X/aXw^K]o QTVJ?߂/Ej.I`{AYv,f籟}dPOuB=2]'o +c^r\S$1^*@O&ȷnL 8o>}:Y4t:^[WsNNę(:w5RP1F]j:./xH!:z0gS! 6iWq/<#[VA)h-hO7dͿ .s %4bfkZ v*t. roΦ)?xHD۳gR*UrKe.Y9"VXKuU-%c i$ҕ|-{Nwd-tp>{f 5&[w{5DlnP 8o^(nTƺ9>pG%8pX޿-? .q E3ǙGg Ke1qCEc}pmˮLNhϩA7^ɕ*#: 5(5j Jz+G 'wgr-C( 7lb ǧWCvXj<8~v˽ udd;hjkTz.mD+3cj<ޓK73SS@Y ^$ +_鋦Mr1f |=&knިC7 Չj]X /Ipԋo4:!vvۑ ɠKvKd>tS~ {2uѠ`*3Ǟ%oA&!Oׯ>ߖOٝG9YRE !7tuw CTYwN\.SU|5HJi+j% ^cN`445C~;zIܥEYА H&$WލkhYLy_V._y2W*TwNPz.&j k +Ay"f-v0NCj @K:!׵2vz Esgŧ؛5џo^', N¸g.;d\sBOZxioW ϠA~s:"!8=t:}.ɣ ĺ)54toZ-] ?rSPe0$/#Bj7JODnZ +[ q뗿ډ<kqJO+WFa^ɐ\Ts$L.GWt (\"%Yõ}k_$طD)*!cli%YZVݖb;܍+F Yxnۼ[KNhn&vI5K={&Y\ݘ$\3a~RREJBBPx"N{H;BK[(޸:/ӬB,AӀ:Qf'Mk68ip#cqM`,'W!J@Qhp6q WYtW>vhLV#gy.X릩C2vQH21{!w}f0g ]V*v@W݁/nbHzw-ߎUƂQُ-Offô>t9S"S~AyCK3@iL>xq\AmS6~ G n}i)3q^@tI$D[Bp0eV◇_!ݿBP '!vf)_^q3gzHh: m2Tُj5 rzb售eR4'6SOuc;b'L2Pd Ft`5+JD+Pmb&Hl9xG}1hr `03рm"A),rѭ^ @UF@ɟ۽sZW)߿Ȱm2jDtoѿcU-Hf7ԉ6. ĦŎHLZe |NkU-O.}'{9XkhiQC䉶g=:4ڌԪz2 `+H-/-AU=y۝ؔ .Jm[fI(-yCuul[ߌzɣh]Z Cư1#xut?U7=_u| *1>5@"? HȂKn+˱W>ŝYP6gX,_@ yˢ-~WQƙgI ,/ ӦMCQKQkx )) l7d8 B 5pPɝE"PZT!oWg "HoLQ4 huXkx|2S톻o~;ތVh-pWb9Aw޹*d{OO".D}dv"bO!֔0%ekJM3HW6>Q 5рv\X[Ϸepvk? Ci[֮2gYM&X?R_Rl_?K].Ң828^yۧr2,LyrYu+}~BY" &)%\AXrK8ʱUh֬)۟¥ Pj ] CWYvgOA޼G c+o3ILY@N3ڵ00잘??͛D yey^i zvOӠJ&7sqh+T^># lf6r7Jj^x<p6_$mP; -A o ƨ^8_{쉩_c]$V7"|RWG&u->uUYe 4ʧn)lHI淝;¤2b%*ɧUVږ0X1l|'YqL7zXQ6%|.vq@ԨajPM. .FR׋Pfijkyh &l,₴Jܵm؉\@Мy q8ꂓ1m~(xpn ['k[ƛo nӶ= ~L,V7Pk&˟ޒ۶!mPi#5LA원b](tRB 5(=DFm?p|d a} /JQd Z:]$Aw3@D۞L4,yHVWMN}VN|n]ր>|ɷ ؂]axSůb2nFM42qX::td}<+̈́P~2#qlv ׺VrVMe04x7[m^=oy|-5:TOn8RL/ 7Fewv _Ӕە嗈&fb&l馭G*@t@I!KP? V#Bᗣd՛>H(Z".lv`k~8_q5KVJE|ժp H<& wo&faCМWpͅOXb%yJz&&WZSҾ=۷CܗHl=V3W#|Zz_h?y{u?VǪX?qOV]֯nqu&6a-ֵO]u=[*hqxTPStdڐƈ۪ޖnTNTG^;ESǁaIVj/y6coC; @@5@Ll`_hȝwk[V洔6Z^-+`5$&VfS] K ,Ψm>7tGwx3q-ݖ·{ : Lf2SezgaUs Ո{r_A世7@ZF^B <5SOűbĥ'-1#Pt6ON?5 EP? Z *.뭀[ZlaNbNPtUN;<9}=Pv5J=S|'SB$Eq=Xؖuka274`1B )k J#'DCDѺ4|V]=]r5E<'Ywլx+cpU.K߂s=\ Ug%V3O.[) ); ܹ:k:  `P?G dee OI[>u ʲ;'ʿ)MatR 8y+0d:Y%syu2Y!㭯*Ǭ|;7Q Γɞqʧ H7a҃If4pEAVB/ ,Ov&>ɮW؞9C|:Z|]7~blYIqRϛ=,=:GՎ N=#39ҿdq!~E\X/Қ7{5# %ӓ711/7㆗}'B\Nz C?u 5<t|s} `E`>U6;pi,L6:Ȫ=yե}@QK'- tF*-JWy&TI*UB|d, g׋iڡAUOL yY<$e j BP~^кc&7oQj rޜ7yl2Ҍ*TqKs~j 0adc$#714mwVz4X{ּ4p&~y[˗gu]tV\z(:Q8F7zI/t~8mHyyoI]h bvβ1 e~out"D[r=xgSKyl}Ջ7`nNk_h#A:Z=eŅ޽@tHM!SP?W $4 }LT,b2Y(ʃ4ś|Df "Yo< #pa؁{N' fΏ~.<*8m꣏QɈA?ˏ̞6 qS}t)]|rcbԤҏ?i|LL=3FdL.Gx5@ƍm[O?lmk }E'&V< ˸Q%U(J_|kz_uUϯNju=勗۳SҝH.?rx`t*kCkg5+Ӏ^T-NG/ ziYZNzD^IhpO'dp!,6ϟ;1`&zj"<}Hos71'WLNq*Kj4&*qժ#{W=/xڡgnHlB 4kEqIP5skVg|}1ǧYޠ{8@]xnò*[revy{~'+vU//dF/OpiɎ+Oye ׭\ ?Yg%PVj /C1,T9k* %5q>@v\g;=f("qݐڤGxW͚o/]:G^xx6c uS[ps潳1\ }+UB~dAKKSxJW=t0}E+JGvޭ/c7ҹ(Y19BjৣN:Mgd\,Y_.⣶T*zYue'uoUUW廕|/ Pz-ʰ:z.Vz=YW&݅K/8<$z.|j,5j]`;MYLl2!7lIfO"=Uq{ZW W%<O}| RVA5Wu6 0_^Ls+^q@ < )l)r $Pѭŕi>; q V$WtEeq6˭􄬀%Pڄk ?\gaP~ۮ(Z}%ʫㆹL8%Wt8Qa+wZ~掶Xl-\6( j>\nYUɮsHl}.=c 2hl]rh$,u3թ5b7h$2zhxܰ)55puv8h_Qv4 TMZO jfUI6dJQr@ō'\(﷜ SV)zpGt9uO!06R@o+as9b Ah Ns9,B k Rm*c?GrPM1vʛhik3d+. 96ۥiHOFF߽ړ46H[;VܲI!Ǒ{4MBMSc1=6s6jd̗/.%Sa'gCҩ1J?x*fi 4B w4P^=\yx+ d[VSS\[&Q& 2]M[G,LκհOG}K39.wK!\9.]OuTV? {g.ҵŘ}X|9 Ǒ'K@tJmaP~M!pWƃKqQQ󑗗ڵ ۷o ^xj @ʋPx:}Ż`QdDlR2ozTxg(Mj7~>!Ǩvq,\= 㑲{5>ŎKq^۽t]zeq]N[dg|jP68M"l2MNEW\+ъ}&ڟԚ/74PT\|㦷*odAPK,̲b ӷ5٨t5Eo 1xTdVOG-G,-[XV>_w +rvnYᦛnƵ<d 6mWjժ!==+=z~ξE_S!ݿBPB ((8oɪ!Hj̃(j [17{+vyXU]_M@+ *(R,`b-4h$X&&b kbƂTڠ JD` o}~ͨqf{Oۧp~9mȨW O?N?.A5PxZ.=\OqِIŵp~)=lm7/?e~r=}4.s9b|G˛}h#W`~݅ kn#IGӀ69UPG#gB~q6P2W/ˬ疁rzit |PxW~\rϊ7jn hhz]Wr xխ[- ?( .cנ(E s?(mb}{ş<XgCp.ºw.O֘?y٪sr蒟'[MrB~jT";_<$Vnp\VZM:#{H9nx7!־xgr|?͉zX?rss1o.x=HO#_'Ex8wl?ab{A.dLAc- EgɶGQuzy \Q{pƙgavBg9ςn&?S1ȴt ,-%/)VT,xfYayxH WRz޻3pYG_O:)tfWA%]Wh5?`ֽ(~xvspܰG.I?s1Y8 y;cTk< fG8O^Y^X4>Ytxf({o9|tOp_t,dfB3͘!Op2u36G].ZO*k%iZh܋ymq#Y9ujQ$;_8!cf*mIjZ8!ԛk #/1(J}o3wߌtP߉G;;u\W`[ j>zYӛuOEWT2'."[`?k%dKjqv=71H率2LzWatJ'sr J5Xµϲ5(A6ڼ=s@uPݮ-JAM@:Yi1d/Gd1r<[vЃ+4N< '[k }>p %_ΛcWٯeKB f/pOND~ #dlͽG; +(Gz'.Q:YT-)N7l?')]1P7?:-(ȕ"U1LRW`EnFA7M+p%ɟuxPxL긜 1l^R_l5;LdǢPòu:\СC{Q#⨡x+&=zMc*eL^A5\c&f,oҍ~-v&yٿ )_uo|B3]+М dn6/EЋP]xyoel1Hm9h f- 8Gð[K)YO1Oڹd+?헉?12{x0J:UD}mypZqCyؐ~KzuaO'7xR"aA=tB>L2zZ6K&<DZGsf>G1Mܯ~iw*w @*%^Ԃ}'Yr.cX7[rL,iS 8#Ƅߓ1p-컥=< G>i7ŊOeC㏞.Ǻ q&KQCbHkI4#)c,9ͮa.Dkqï]ڒbWf`uA_?^nQ.,NnM|6-߾ –8̋O? ȫ.O>Yb{V= ù+ ~>W5܎[7J~/C~v2(CM)o:p=U Fꇕwɷگ bGca{c0|װձ/8Gp bsmCp~ݰip+kڸ& :')u%ApEϑ)k ]0>stӟna`Aѱ @zZ:M~2Tv)yY}p>aOyM_ǃ~ 4tOv\W`Uz-MWX =nG_9n8O,?3[32=74,74g 1~\Eh1,/ n\'=q۳8EZ.ƙ۳Pc*s`jj TH^l}m >P[[!G $ lU[h@ee%^|E{$㩧!˞~a(yg|$r0 gOC3lGЭ(/\2S1gګX27v,N8~ {! 476)xWp6B2r Oԫwd mX#٬xy ge~s Lxv:d8ͽة \#?zVi6j,YxӞ QxW~]ထx򟞺F^ޫn0)^U}/T^(PM$_:ȒS֊Ȗ>y/[}X8p6+'5j^xgʫa;`6z[l*'nM/l¯M[8O0YQ^u娬('rEϒX`n.MoPZ#بpF8|2+ 4%ftkj#*vEH|oo㞫徲%}:o^LG?kUɜGw  OGԵItI'_KÊ[a's^ };l_}u5w,Cp*r7:lx |%%%>}:1o|̙/^ҲRamy*׭CA۶rC[ ѻwol]cz(/@ۣ렻›+n T/}SQpp gWp6@+Q5~u'23e[CoyWoIG>cmw>(~z/T+z`ip`lc X2 _3W;<0'ێEl1- ]t8doUPr?sN!ocv4 TPJ/pZ?\6ڌdaͳaov& Ђ.88[g汱Wл99,LԵ7?K9u'Km?@֦^[t1yl96/+rY3s߯:u8 v5GgٙWh օV}ɮ+ 4VST{7*g#{]K9nm5  p[9v.n7?8;,7 +xԵR9u)9uBAUۇ-R/ڲ~&j%o?Ջkiΐ=k%`I[֢r~@1m7W/on#d=Ƥ@so]pݘp\u^|yV~^8nXf(3#&A2Y%_sEe/p. 5+?3bE8_Ԯnk /ևrӘto}tHːѲdu.b\W`#RAnDק Z,W횏QΝ( 7A6Erdk zѷ@-r[$S\7g| ?{ƍE ܊@ <0@&6ن<`*ڳіi8ߜUo_>Y( 9rrF% -N趸G r\WyBwG[^S&Q{G(@0`V˩k7:u~FXXڧcSSZV1[I%4I68V˻+-BƉ$du.\W@s{ttWOu\W ]@ز;˾EެpgLc@ku[٥6Rȭ@2_uۆH -f%t03P[FWʗ69xy{@?WpDB˩ph;Vwm o+*H%I$p26@eA'2#!5fZV7)7efo1,m vbn&#+y}v]}w=CHe+q+[6s\W -jwUMB;wɗꐷ .CPAI§yo9YU1ic66bAnc6lOȵ/Y^S 8UN^ ^p 8U>y]W`Q=ͣ+l @74k{PuԤ7̥G6M~oϞO)p6ܣq<+ -Z2(g+_G y72q[/ُ$R%aL6KMWL5O+ czFtؖ ^A v&klsb?y"27U/os]`5k\2@dm*{YmJJJ0}tcX,^erLiFAA>ڵkۡCBE}v@>DNNNsOk {t[C%+1*6|"*gqß+[zAzp4-o1i-o''"B.yc Lcm0{3ӚeK.}[Caā,6G'xg/a0oB ڭݶ{D V&mA.ڴBY\ʵ`(^ ?,łU3w9{5;ÇG>6=f>Wp\֡@FF&r`^%KQ%xKn^٢KP4e񪂧-^_VaeVI%CAfZaOOV[M'qC49[iw@]Z"?) x0ixꩧ1tH/ ݳ+~wmy@6DŽ^rȓtrmW_Jǘ:%b{?~< ~sJ~*^ +?Q )l̳  X#~)rw9dAJ<rAVV\x/0~xZ/ @KT #+}GFqϠJxU.n%) AYANVlrjҞ4.e;h,#- kݷAECgTؖ>1CO},Q嵞^Ns-ISt]Wh- dd˛ABE]vO1eTL<4 8U7vQ$ XK-ck˼+vaZV1-m,t!P۳'4Pr>({xT/&k{v$w>^w~Z/Q[[ˉzh 8‡Kv\W m+o18LACFmMW(gLF $F3+UkbTR(k۠d,zM2kW<^qۡvaY9nx7||m *g߇4Wq;T{xLǶ=Jk'%1'UȬk0K%AV6 IG^FWC.dAEqC:iԌk#K;By .A;q!>t9.ȣ@\WpZ]fQx;hϵ^%.<־h&L|RfGҲr0X[֙qKmYmXF[CqL*$eX}A2cR'y2'o`|6)X@9v1o/]G- WcȘVe}a#y- u/- } i)KnU>yt Aö +^׉x x7_(ov 8# + Q7J_J_{UKe[HL5-ũ1өn,(ƌY`Z砗D 굝͓uR W^߾K.eCMM FsHd*[Ca/)p\N(iP_#x^u~<| [^v=Vq_o i-7/#UW])zh 8ks\W[_+ /{Fm Ix 6 Vnq2W)=D@e:g홦΃6B̂rkTv۩76,C/ \&=)oS1 `a5@Mg_ -4mA6 CvX ؉هӐpK cgN? ;o-S~<+ N=aCЅCP#TsJ'?,{ys.0A$$&72Ob ԺT[m/&[=U7ة'7$h㼾}G_|!O?!ow/vzH1`B\+A\B(4 ̉mʹVZ%oak  Qr.y*M4Gz^⭶$)vLڣB#(ܣۢ/p\W`C- ?qûuT|76U(e%/zm[ԘufHKKxI̋2KMJ=А6ȫF׬k㧞z =r4 ]viBo@n:!-v)S8~V o(3/m"*Zvfoqh2-:#cr\ Ǐy]Xz'0?8QKR<+ + dd ǠQJ^ze^BK섐Ij e4g jtH&Dfs b)JoL3/WWGK.,Сc[9H+r ,- JQgyvjG%(R;B3/'MmgmYf@l*Y_yyh?2Kyݺa̘~e-Kݖ<}5+ ͤ@V(w@łb<77\IJJ^3eXƀ>RcWn NVҌSWE%J]EYoԕ,e_ <PwޏwV'xQ'R'H%YVUT޾Fe!`bjc! ̉QN{+WdI +lk=h߮]Z-)8趤kq\Whv2r G/o k^xe_E> 8*VXG6DVIQb̺XC&r{Wk+C$ɟr U&uȃkWHW@# 7;u1!81~>/Ș9Gp7Wh;qeI\?0sx׿,i|⮀+ ߷ٛ@ۃoAGy/ovYXcXWyr)ɸqy- ivVn}Rk.Նy}r}4<˗W>WQ '3e!ysu!V/%PB3[eد#'5kl.Ce? a^a )s wWz8=esin?B_+ @F^!MCQ SPd:T.Dd0֐LݍVؼ2)aKl%jANH늪yOu"gS藰 lRI+CyKM '5@p&h{z} H<ɾ~aݼt ^J;_Yre7v'ypn}ޠ*6.i + @{Wڕ/j[w kIe=z#[oIGm_8yBV΀bi,6, i #7*fE\wN:M~ ҆0Ip `]$3!Mܰw֠,dIOΊ/ c5ʓRcWqMV ^ uQ@G_C&)@SƻqؑcLj p\Wp?<6ڌdaͳrr9nEKu7 6S& q4Ӳ)an_5֢_\h#- ZolgjqCir%Y/{cÛ-T7ZchCV!Nn1 xqVW{&?<(گG % klȹ?mGno1T=pMw\WsF?S"~(}%L~ϗa$p iWuR' Ϭ_H@-QDJhKw㱯0/뮻 uu v 8# + Y`.Bzp'R<~ь%?['PfYw"G{z {Y C^g22Aza ȄQ^р@Vf@NzYK`0R+@Jq8\؆r؂$)a-_7|3=-ك+ + | dd"wz39NBl/%pF>SF"(ePJT_SŨ\8_푷h;(\o&f2R{]gc-2ƶ.')y&١B,kcLoQ lkٿZ-z )z3XGH10ֱ,9z;2duq)ࠛf̧ +^,r{Ų $1x)ɍ14dd5*?} U_ufgFi*VVʋH!Rog8 b#v ˄dD e[zK6%1MYD|-җRq Qh+%}KMmB0&LQ.elG+34!84{ク[֪~KtQD]WpZur\_nd$8e̲L03Z|1*+4[0n lJWZ&cEfL=B\cڲ?zoS}:.%ItzZ>WWp\@}] *g݋Gnf'?&duֺ`>CK)2žR_J:M4 Ъ+޲qj>kQMyze-թx%+yƊp7.I v)m̛˺402 Q"W(..v5}(vMSu\Wh9 T/xFY({g8) wx9V`|rJ s |Iᙌ , &x j'Y> cy-Hfֳ͘0>6V)sX!IۈYa6Tקrc~ЫW{̟?/tR |j>YWp\W +]6]ݷé#oITgѻ& I )f2VZ/5ɥS*jFȮVʳhZ8STYq)`@,-a.«'*b]bFl*bڙ1c؟dcIMu\Wh) v6x}555tӶ};7r*ɓJ{ѓil'e- @ V}uTWW:Ӗ6:e2hzq!\/{N \uD zC7ZۊGA7E4Jօ4zX>UWp\W) t *)!jw>nWn_ֲ6 Q] ?`L V`ls-EȖmy,s(]=W!N(8#mka1P ʃz1r~A;\VV뼸Z8uvAb&uZbN{&X#]2VTBvXv =i|+ 7)Ю];USp }6{h%1-QJoZOJI#*; RtߟGe{X]:8=_zU1;- {݄5J1}kŌ~-SHBEXj>e+ xcCUȜl]%Iz!q6ԄZ(+#T4,ni|+ 7)@-е9%d`0'1]iV2(>F6eu7G8xq9gsq=z{h,'PqYؼl o(0{&Y_ '-ETgj؄$ȫr'QՖ kGn t{b>_Wp\W 薕 ] B..¬U;A#PG"#MHU Q‡^8.t {޿v>=KuJyk1۵n`d gv9wLmYjKFM嚢]z#ŌIݦ!┕9Rt YoMlҡZ<oAGߕ#طs_m4gFRX%O%k4mbkoᇥg;xH3tt]Wp\(ЧώXpMD<+#313ӪYJbN`T@ u HxpW8&s>9O{(= 62n7#IDATcs A޽ in=0+ + W_[`-[;BSb=_j̴m5=Y8g)6XQR_uGV :~5yM:mc~ k:q#F7zˑ?e%4hq)_FKSu\Wp{Yamy5 ʗtz_ j ~mHjJxAPR0z2«ѷ3r1$F[!f0?zu5FSUf/d&3 gWp\W ;<>6 ᏝMboJJ+a0ª+8FT6d={zNi)cy [> .9Z(W``m|9v\%;R[V,^ #FHitRA7p\WX津nqB@2WTAydA41j'ۅ>l ert*j`kr (|sU2b߃1c +K 8cI+ MS`ܸs试?:d -L ܆KِH~Cfj==qX\,6otKu=cKR=és\œt^le}_{-zd?$v!MpMv\WpqouꬤR9Pn|r ZjgWJq +&x/d(e^LWƱfyj`]jj+S kT5I TKzOD a(i|Ү+ @vmum<줱bbϬv $x&y;J{Bf2OV IOpbAdڗҬ?&ع,cO-@;>ANglDC+cGZʮn.df:*iz󧗦Χ + 4UK.ny:k$iŴ$ ePi}Y@S=u ۅ%ژ2tbl*(y%ڰ6lcmB.F/é 8+ MS`]v; әg< $BR_ؿpHmMx--e /{\ ,(!<6[uDgA^FK_SLs患77eq:='~H%JࠛЧ + 4E/ k+ԉ)7*0*\ mB ,0IR!2 6%ҳ"3}؛ku MbBb]ľ  ܛ-|-pMw\WpСC1|ףg7pa׀׀Mԩ7 kv`6,c}`>]}S+elmJs2OzguuNԒdY7K { ߢcǎړ[~~>{Wp\W \wPp0^8 8_ƤW׺@ɲ`6ڗ)l%6p vU JO }4hdje  L \|œ XLﲾ},C-[+ࠛW + 4I-?E_R$j`r !VTP=ZbEL y_[:|Il|f&}5/, pK|s\/)sд9x׬Y_]n/ Ѵ 83+ MVࢋ.FiY;\7vb 睞cublL-ۂ/p\Whz( F(***9 gp+ &Y{jS4$UiB0K:nHݚ@UO2;k;eK, _%X\ѹ){c_CݥCKSA=Q_+ +D:?$JR몿PLB$ R/}(&FD{BeT+Ǖ7+a l9nbY/) hdJrC1j(i%*p\Whwy22)?z &3e2='%J>E,6Z۸e2OA}$@9'>ye:\Y>;ބ>rjrtTA7p\Wh&/ǟƬehPS-Jo i>W JY)l[X1*Jbl l LzhuN fܖUr/Lgo%B{h 8ks\Wp@nn.̙;G=j[J] y)V,4xZXDiR.ۄ@5&8#ҕex7cJwϰψ{qi⬳~̆Z-\Wp\(׎=?Ы00|m aˁt Z×g7hYgLO+v1:(&`؆k}H~kes .3Zޏ w{rEt[˓u+ ߠ@ff&&N~ z{~zE3 6QLBePꖆ&=I; Blv NmԆ>!Byآʸ1w?N:Yj<qI:]Wp\&*peƈ#1fhuǸ}{v^ 2#) kP)$mY7!|s)s̈Y8z$0KPĺ?XEN8ITUw̙V[iZm=W + 4YCbhq{ G³G{xl0nkn' <3+^y rmCog9Ӌ{}30p;0l1xr[{t[ո+ ͦ@N`ҤI8-qU#Y:F8WtS5Zσ$$7%K5o-Qf"zp93 YmǃoSv\Wpq`G]t T^ӕ҃♵rS{mO./e;>t/>,3G?5|sCnp5tmW + 4rU1]vs?h2FW{]XXSnqݗd[B//í`g#?'S,-<^|B}rWp\W`ѣǿ7?|>(]$_\nZ(¨z0MנUz`v!^P Cyxs] = 8親iWp\Wh]tO//gO ٣Am<Ƶ1ΈaHG 6,r)o|S˰R{x: 4Iu֩n|jWp\WYèQ>ɓ_ƅsѣ{WMЫg'AvhWV rUaxlKx]~.\},w/p\WhӦ CqШp^>8pxse8-H0})+ Ƥ@aa!o?6y\Zo=W + @RAU=n_+ + G}+ + *t[ź+ +zpm=W + @RAU=n_+ + G}+ + *t[ź+ +zpm=W + @RAU=n_+ + G}+ + *t[ź+ +zhh=J]Wp\WpB ]-̆x$]Wp\Wب044|S'ߐT\Wp\Wp\\ݍ\Wp\Wp6Lfz؃+ + +T#w{[n ;vO>俍u+ + @/_iieoi[̉0ͫK.Xp]Wp\WpZΝ;\9W](5f`_~xwn + +Њ4i`Ɛk>7Z]2d8+VǮ+ + *@n$?:4Ɨ7r9ط] nVVxmf0`.]W+ + 1cv :Nɔ[͵IpBrrr: :<̦m\Wp\WpZW^yG}$y9|it,8p*o^!wĉg}|C+]Wp\W:]+s=m۶E^^ޗ kkٸ|=㕟BL0k׮E޽qYgaXl=]Wp\Wh ȁAr!'駟ئMeH̹!!C]ؤըDEEQVV5k`ק~~K,Rm|+ + @*@Xo;tݻ-B Y cv~K]_nd]n]HrBҦSNXnֱK`6Щ{;Wp\Wp\A2& vځy^\ ]N&M6\9qzA{+ + h A^t"r̓KV4X_o.Akq`B, cBygi &VUUWlͣ?߇u\Wp\W 2f4(Pk^]nS<Ȏ5/Ns@s"sB\@Gm+ + @z*@Fd0VLBshͳk]覂Q[.'mV9QzqCmoHmg}}y[Wp\Wp\5.t yq۫yx\2Xސm0rP)ƠKR75ڶ\]Wp\WpKr`j0X5е/׶,0f_l!IЃM`y[ 'η,ryuѽ+ +  t F^\K36o hޣX[Ħzj ,6k}4+ + @*\E5m k@5b jv n-v֖y+ + 7. x 3XmUh6е2oWjz + +rH]y2\}M΀qLm̦q]Wp\Wp[ư~ľjrWp\Wp\[TL]/w\Wp\Wps\Wp\Wp6bt7Ss\Wp\Wpt7\;o + ++࠻?+ + +࠻yKWp\Wp\Xݍ\Wp\Wp6\ [+ + l 8nǧ + + 8nvp\Wp\W`#VAw#~8>5Wp\Wp\ W$4IENDB`amqp-1.8.0/docs/diagrams/003_weathr_example_routing.png0000644000004100000410000052464613321132064023105 0ustar www-datawww-dataPNG  IHDR3=iCCPICC ProfilexXwT˳.iy K9gA% * $(DP HPD ""("*ow&TUW[3%<<A@Hht~g> 6p@kmm}Яq_u>d {zGy>^ `H\t80f_Ɨap/=V3Bf zzx00X˟  3{Q ýhx؞臅'DG#g%%''#+kM{X#UM(K?2A%KX􊉌K 3` P:6V87 ap A1.h '`L0*X_.AX!v!9H҂ !sr< ?(Cǡ (**&tC3hZ>C;$`Bp#i BaGD!",DqъFw7Cp!D5mZ" 7mm m+:j:!:]:7D&a5zjz2>=(} m)M",CC&5 odFCFo3.D>ыxxĄaf2a d`4ĴȬ\|yBf1a f92ɲͪzuuM͇-mmݐ==,CÆ#C5N&NuN/tF\.1.[C\6yùs^a dzK }̧WoEG!UH)8_<uA^A ÂυT zd'I-[a6aDZ"mJQ 21Xذ8B\I<@L|T-**Q)1%ԕb2J%AZPE:GWLeYFYSvrbr^r%rO FIm|.(L+-O*(PRVTSZQTP.URaRVTSE&Q֨Q]R=H[ a  9->-r9m6ER{AG@[JgYWT7P=H-}5#]Hct!CFCb×FF~FFƊƇ7emeRcnlzάl\<ҼaajqⅥe-+`ebujZ:ºccmSbVmW{=3"1=t5[NNNsę9Ry@%WE4Ƀ>vp vNNqo@{8y\NTR6=M=};"}~%W,-,z-z}›e嚷ro;ni5|uw-="?|X߿) / _z67_~ ;}ʷݸE?D4b/do/I@g/ 8@G x o0vUD %Ƣ?aVSTq[x4LkFM_0EabNdg]fpr y|HYcaZ:Qx&^b^r\tel# :JbD*Kj5lt$tyX Q?5362hqDz5kv7n:695ovnvi:z`[{Gs/{4,A!ڡ&a>qg;cc?S'%*1}g{;W6{Yr2 [XUk׳om9mzv^6]=N.n{T7xje<87j>f7eum}s ek>|b7ogW޿Qj]0{_7˿rVn3? {qLQpߋ)W/TO`A'A$"&**&,N2˲r} UJǕ#T) hjlk.jhw4^+/172:i/$RJZFԖl'h@rux=}ce'łBCUôÍ#l"ݣ£crb+nw% 'Z=}KN,ԞeOǤS2,2Ude8>{!EgӅES' _8T>Z1Y9 g+;ըk}4pj0ghHhXx‰ɌG=1ͼӂb딥7W;̽zAܺ'FF_7o)m~!}gAI_ɯkpp8 y `kw B(@ a.ā2\w`l@8APUC%!0k"[C#!ȷ(ARzk ӄz`;ب⩞SkSWuQ{wQAp ]*=$#7c QĴ̜BRΪ:̎cYe̵]c[@BZHlDp{& mIT,+Vl`Es%9e*:ISZK[N'P^~XlIiYyŴ5YUؗS\,dq3ry{f-GFVGbb_' G}!:YS~fg_.辮\ƭĬ.}pY۰vyq{?w?pD!t qZs4/$i@ǘbJ02Qqjf>Lӈ`X#Қ>ӣף`0HaH97~;Z'Bd3JQU1بxDVtLg~My7_=`:__m#gooro%x@W_zrɈ}5$lAB"x pHYs   IDATx `U a5P(b%*P1@5Vc+Hm_*hEBZ DP" ( $~̙{ϹKĄ ܜٗ=w3si      H2ml. $@$@$@$@$@$@II8)&            HJIl4 1 @R NngIHHHHH(9HHHHHHqRv;M$@$@$@$@$@$@A1@$@$@$@$@$@$(h       b      $@AF Ps $% v6HHHHHHcHHHHHH ) $eh &'wZDj:)n1b`o_{ ^WA x ORU{7Pi$=aH^cJݲ^ߖ) S{?Uظr){g^1aDz5ϵf Bܸ[@۾R/ ׯ{aLz4@$ڄ$eh &$PcbjWDFݑxd RS>vn>(k:lZ7p^P搷 ;j2>+x-As(mDQ'"[2 kF$Ey 勨$hЯCxCFgK ."²/˃+?՞*y'ؽɫ%zT^?oKpSଫ1UO ALĦHω)<[/t, M^Vqnhr ,HF׍c Ԋ@N=3+24 7֤܀w1^ݸ;؉Ec3l MTXLsZ ;s7Θog܈7>)2gW7hֱY+PsZUF$@-q V/E4 "-,+%0-y݀;Ʊ Kb|cVm(+adNrpj`[*@#PsC*'";'99ȝ|rsk<+L$@#PeT$@$$5tb7NI)r-^:0ŵO24sW[3'@XNФ]o `H"n61$@$P\!?;$-qcvsz&kEؚp 랚Hw><=X4ỷ-)jCjӄZ˩0kB@;7N̬5 aF+ćY9$ uNXŔާ`Fֈ ʶ2 ~RWmSȝ=i=O+t8r x*RKF ыSs#b<%=-- ;61$=q@1ꨐD Komzv>\^9ur*^b kW<|kH@Y &M>,yEUOvkT`풇er i{sc"}y=- ?aeXuGrAy{=a~7*ZǟLQeGoTވ?o $<]qCq! YA9RQ)ײ0=uGN,I6 w/o7@] o@c10p nO@ӹv,]ߗ5G(s'm| EKrNȯW%ߝxFc3}HH^K4$@$JKd>Ykٗ̏",k%.(p,G,*-)_#9v,C֜P  gfzmrmcFv~hՎ@XGyhטP5@&\KU募0m۲<3C6%X~FKP~fT~sBZɵr46/EaaN 5]AʼtMHnCdԧ`MA'=k~yPdϜwqu~ΣQ(.!Sbw\Ț#}ʎ[VN`|߿١\E3C1{{T ˕8ڠǨv$čҟrkUӘv|q "B9+b(Us s^Q96Snx.6=f}u.DFDH!3: @<txF>3Grdá,xb^/>)q&T2cbYnT\wf߀UqSA<_T…+Ȥ.YX;([?b2"y(؆ZQ3OK LsUKŀyɕ\Vr?';}~ܙX(mY4fW8N|FYp>R~K{k>4Z+2ۋ#ًLm痐{umבRy7ug ' ANx@L:`2@nL(?-(&(EE(ω`Hܾ2̙¼׎PQ~p0"LVVn^59 7gM>IjIvQeq. G= [LvQ`V$Vx`g_֔W*o'xd{o!Ӧ]5?ˇ .aGZwkI;iJg/Č iE_^ | rn8u11wӆۧO JuJLxLWO (Gј>' Kgi̚,%tp9P">͘}} *;7LsǷ3.}[uBx▱f^<h͘sV#EW^e'%h.kSWwvX13Аy1D${.\D$h⾖>;8ʸNT-`KAGw1zc/p؅M[Ťa?;&dk: 4DVD$@1ɲZ~UI16eY%Ȟ|&ӫ   [1tĪ؆e=~XOvĶ @ỳ}{`{Nz=۷';wö-nU KeJi_f)"$am7m}=bbx5s0n쾖>`͢\a(&klFc \""&.ةbq7/<i:zvYa.Bx \;b5Dz,ߘmϚY^> :Tlǣĥ7kP6N\Z6v~Kۄ}dJKG3c{*3n] ¼.qP{yq~g{OJ'nLXpl@uRS"6kԐZ`9Օr\ sjp+uĐQ({w`MHO2}x+[EQm(~]qWau_b q.fAgwX\<%}lvT,tܺ@bl'D}(*+: w]wW@aR\50ôRB>$; սrv5ȼYAYW[.q+nYKn j+w8m hm-p3(~@`sy'\pdiK\)]\c:3]s6Lul^f>54=n*k{ekƪofeݸXA30N3&׾~s`X~GA/޴S\}=:LoݸU Q?kkܚU{}GzM6u,ma%M]eO$|e:-&VB #'}da3Y]*,si"Nt߄=#j^Z7W8Gc,YK,Kq/o nΚpy1 ħ& #_U1=#:xz< ?|uyl G,l-{'̜pj=_#gvo)Uk^MANPލZ {{ZR;WW$@$p gH@ߑb22'cHu78_+:^2>ފW2+Q)c5 'ND/d4g `-W/}YpWai;cO۾v_ƖR͍ɢ6qbZ#a{0.UK#]{_1L  / ۉ/^yfyMџi_&L dqwbc` }ݸяE;Hh]<O&8\2 L$l_! c'xerڔ ]=7XCL$@ &@A`̀H q*NPc駟iƄ)cf_<CwFW-bX߉feL3pj`Źo-*nW[(NvVT{8EJW XkGN -5jcpս̫"!#oo+qu/V2c"k v`?z;:;.sf^,jl"R%6~A#ʝ(RVE,Ӂ/zgz'8ao *vc}7OsCfs~bo t%9pcT\ 'H@HZ0͋rBrJkSe\U}U!{9L2rB3 f慲+SжKK͜ʌisf(;;;T,sL㦛SRՠ4$8sB.$_hGE9>NkvhMXFxz7.1nߍɬP~~^(+w wuk wG-,WT.39wfhD\YU7?dd23U&vPʥ;o 2B"~9]1A$@u":fd hfNrj+f ΂U{jlEG+B/~5 ]13- ̌-730*pŨ+Q%Tc? cëϯF }(vWJ 25BԸ}] ƱnjТ xc13_5,8=ΜVI sߨvf^A(?;7 PW.㍂'g(C. F'-r!hROC^pW!& ;kgN_,SzX/k5n u;l⏾QYtfĪmvXw$+;)ZrqxHC8q^i];=ާCɠ=G譨9 7!ql 36}]> ĭquAbɞ_mn?Z{flس$xGsmN3^5Ԧ^ypJ7/F[P=o@z[y;₻E}31S^kqʑǹOZw90>ܡ=*+݆E3s㏥,\ k1QHi# 'F%vgIH!n|t@ъݎ: 3-l7l'V?׶}l;tEpʐS3(n7H˩-QZN)Թ; Vt5Z5FƪX-X+; ?WV"ĵCؽظf5V A pdߓ15egVaP\6:}I"ܐ1 (ٸ {VŖw6᝭? AQSL$~n{P)9X{1{lL+Z5O\UxEF$@P'"C  HFq2`N@]W IDATR.J]՘ud 2 s  e:.z 4yo߾ӧ}-z8,jNk. @ Pמc ԁ@o _b"m|]:9(u?7"J$@"@A\/lLD$@$@$@$PDW8 }L+eN޹W/ܛDAxyTIH~R\HHHH @AQ 'bjVǚ5'^$@$иBܸ< nk Q>?ZI.'܉EśL W#R4 @1 $XKثM>Z$ɦ ȫ֯X',D 1c's~hHHP7iC$@$@$@$@$@$ТptVHHHHHHP7iC$@$@$@$@$@$ТP`eHHHHHHqsf9$@$@$@$@$@$@-qVHHHHHHP7iC$@$@$@$@$@$ТP`eHHHHHHqsf9$@$@$@$@$@$@-qVHHHHHHP7iC$@$@$@$@$@$ТP`eHHHHHHqsf9$@$@$@$@$@$@-qVHHHHHHP7iC$@$@$@$@$@$ТP`eHHHHHHqsf9$@$@$@$@$@$@-qVHHHHHHP7iC$@$@$@$@$@$ТP`eHHHHHHqsf9$@$@$@$@$@$@-qVHHHHHHP7iC$@$@$@$@$@$ТP`eHHHHHHqsf9$@$@$@$@$@$@-qVHHHHHH4WA,H  6$$  F"ЦM@N@ $@$E8 $@_IH  ;uvwڶmk~;ֽ$ Hl' ԃ_vb b=EGM$@$@u%ή"X۝;ֵ,'Hl% ԉ_:ѫ~ήCvzt:$@$@$@UFqڵkvq @ NgI 'buWUUU{;,Y;v?˓~$@$@$Tyӧ&Lo}[HOO7XqJJJxXQLC$@## @xEWŰ `'u5Xo/w8c8! hv{b˖-x1vX\wu2e tb~ǜ(Q7{@hȉ$#c[|YA &peeå⋑ GuTcHHHA>#\~Xl}]}fXW۩T 6æ+hDbX}|gq>ᆡfj  &"k.]]tbeI?oUْ 2\!ne@SpXFfQ^^<gn/ 4*6}qܡCGOӺZLC$@$@f*Xu}YYYYx2̱B$@$jyENiӭA( @&LIumdZ*+**裏xpS֚Hn {E^諿unWTaI"(#,h#&&Ni]!~pe%56HHug}^w/QaLC$(z0ܕrB& BW+ѐ @k#_;gz='C(nmmb}Iqdn$N Wu'wޭ]0 s1سgOxQ(9FH(9HR9QWU GڴiCJ$@$@$4iVם0 4  &LIupz]=wu%  X*wmv iL>$@@8zm$mcDN hHHHp]a(ncIO옒+nbщ v2~_=Ґ 1 $97!pMU$ T 6K4$@M8'C(p vs}8m h cj8b'A$@$@IG aoiHHcH @?A   VLoXuhdč ّBå'  $B$@## @RHIֲ$@$@$@$@$@"o=_2g"CM61b"wG}$@$@Cca-RhVS/z=_Yp n=%ÄqiӢ݇Is   $$kIsR{e;p]Z$P۶wQXsk*#^8q<*#    $%Dz ־z/Q?8zXYQWJTgO5xi [}_9+;f|b„ ԩS=kdNa]֏^)))^믿?6t$V$YFXGM8uLB~:~<_~.X}{~۝XS|C$@$@$@MGW_ ~]9Ǟ  Ι;:,"M6 Jv"p=-g?Xy;^߅WgY+{&}||BC5pC a]V1cƌ~ u/! J!݈[FiS㹙 }j Fu|Yg_gyݻw7+۷IƛPWQLA AHHHH1 <;5W}O‘' `W^6dzMyP ~|3O>ÛOnwL){#4C8QVu|qr!ԍI5~ljaox--3Am w:׿6TG/8{Vgo5ID$@$@$@$@ii)<BD]Ñ~Fk#VÜv+D"A5QI۴.~xQƎ,҉P ݎLÙSጫNaF pW y۷K# Ra+s۸!Z}ZVdaWSxS VC3=qdΗi?aJ;AǺ b\   H@@ńwT|q̠^&Nܶhp ݶ!+8 ^awTxX`xFzߎØ:d 5S7*۰;{9t2N>4"e.xha&I/.GXT_@5gF⭷BEEEZz1\WCA\WbO$@$@$@QV^kwOccLWU:)Q[۵S27i5:QVxI[ q]4Izv8㇧`鯊0|;߱eoĺZ Zf!vGCPuDj<脟 gzqsGLJvm]!VOvDqhVZ< $   TZ ɰݐ( i|obj49O/( cz2/8ϭ7&W#[Q<ȯ#tEؿ*c bwt#qMN$@$@$@Bwwy1" ^*‹ ۩ 7pB섮Mąwۈ QUʯ6:KW:wķN?~ ~u׭ҴHޑ|h; p"Ut 57p4OIXXv'`Ei5=[l6nڄ*&rTy7߈SN>%7~ᆈam>R!   jzƯqM7W#G_ۡUu_l߰a^[}.|ߓXwu9Rׅ $+fiUW*lufNB&64צ_< nͻ+_SQT? =Nk!=. @Rx̫uDӈDm*7&-: )wOUĨ)vuא~?ktuO/ش\Ϟl}Tj4B_[44q lB$@$@$dTb/@F ZTٕ`'RUP8O XO"Nh4//En+_5mw?o0bWēPfT?諸TNF ~]/]~N[X].ڸ))b' @2݋i6W'ND*"RUU ^Vp:,mpFSoz2]yr:M{RP VPGıդ]WfW>^I/ le}Y7Q)I$@$@$ۇnyw6ѮFNlV`U|"PwNYNX3LzvB p<-Ctxk]%vUbF`Ql뮱6G&qcSԣ+^"hI:^T8/&^zz18|R֖E/䕟,}Ӑv6V(}C'm_w3s*Buq_QGxLN^R3㳢;k-zxk40SWkշΡ*K~„W IDAT\\ITEGk.e1 KfߍGiEKg 9>pmȊ[+dC& ^'n*t^۩W6vQcΥ`V5|;ɮMԻ^5MUJ}'ԍ*#MOgԶk6'}?M#Y~Ak8$eϡC">]#O:3 Qe Ķ# G*`흼=X-vЙ b+ʳ1Z17XL;riVfnkSŬ1^}J+њvIhFCu%įjQkgie2> I@"3|:l" LNSTHvGjW}ueXű +~% R2v*vĥ8c|mYE I<_`X].*|iƭWbm'N_YwekM`v|4mDžt[f .ZbDeːae Ib5ç?4$@$@$p1pWDFlsuMA'JN;7"R%ıknhJ\煄5O1n;NU)m&.9ʫb>v]bejtM&N;}B{n~ͽi6k[o$! OU8pIb: X9qhL`˴7:H4 0/.5.MҹȚŞ=qy"U.C~"+5j U~uJ_mXoA'Msb'w孬 C ?߃W!c㕰kN|<lOh>LO~䍟vr5᧛ 0g. ˗/7+zF/菟 .% d&sGyS(<0E9*&Uh?`=GeF.pN])Vaj"i'`H^Ss 5veUZ)n*ոj WBi\(!zQO}l)֯_o{[C[c_G~Z2wa}E&(?t>t5%O(:0%)%?i?1n4tu!G7 @r GɃM~Zua6:=1A2Q1Dm esT[YӨq[VTk U+%'U]Ex-ML5S+Ú$ʹgl!wӴ^ʹR##x+GFh[LjrWo !?&!g5Z'{~n~i|dx I&wm0VG@%9q֬;}uͩGX wϰG]'Z)8Q>lHA.Dazy(~H'uK.Q# $/g+F=2"UY'zuBS`y~X}PD ][;ҴW`eѼ0v!u|+G+VE?$k a?hVaym 爑 %P'ݪskMFV}ǞSuzr`g b{o@'m6^^_3sE ӓ_I!ۧϕ+EvJ1:~|[uL_-9p:8؁4'AL8v_ϰSO;l `p- qL F< 8oNd"9s[ D蚻U(jmMjom5䠫b4Y)j_{}9eZnFCuZ Wo{]A"$-+#iSڤJ|m4{/2V(V0n/ ;K@{/\?E>Oh4 >ziS"KkzW!]ZWiW`HB ֹMh:GtLoy(p"0o=źsAW чhab3H>m==}4wTJCVC~]Tu=壿7&=WUન-hRM#a1,yDs5M71BWVmT۲Vn6{Mq8Gn!j.Que/غ{7MŖ9Ǟt ^z%!Vʔƅĭ&<Tx'ػ{>~ ӎ@ZU>jZ|x߈bR<_%Mۼ1'ͬػ{+) =&;_=&qG99qc͑ޖ($DmUbmdΠ2'05db/ K kAxbƞs}'ۦ "! $+]R N~SQ O<۟7uxV^YVkRšaZn8,нDyxpS U%̽j)h:S[5ms'QHpn~ggW"vʯ׹5ZPŴ+ļ*xi[[x}UױnOҗexl_ {i޸d㾟OYő3Q2z ^oɫDY_T5>~+F !sFolW/+NXWNˠ4ƻxaPqvѦ|<1'GqazA٨I6~dݹkU7n ! HZzfVVu S][E[Ej ZOԪ-ԒRlbYa)Neq[]έeqSI?&cb0+K"ŢVDӿyГ n6'h3>f0 [n>sq[OϜ}iy.s3}1İ*EwgT  Äۛ6∾e38+s ݦGVɞ~e\!OWFDo >-'ϧmRFXDV鬟'ŭ'ʬB vFE]qVhʴ1ut3ߴR7yI^#d]NQ2u W]TCJB1F`#1Ou¼EmNҖ2<%{ywm}f]q\zI}&0kwVo_>Ϥx%qW~"]6fOWE|W^w+S_d *_?{JcbnJqڸؿU~j0ek&bXQ\2?{Xv"db+>WuI/}h.7oBud>.\y~?$쾵So%*ĻbC|C6}{7⮫ڼf/G)7߫9GoOϕklL4׺6U¿f˴$f DMn<8j7%>acPbW[isX1 ֜91e_@Ecz!gN~:.~'^ǭ>dHH 9 Myޗ3ZP'|GfŨxOkL;1:x[[c30Ot7VE>ZӹbZ-TIo 02iybt<0m4m҈nZ)ifnaޖl[VMq\t[WL^B&\ K4< 7y 1uaSm_>oߍi/ â'ף}` OIY?123J@mQ(qJ\a†.u1@O\}f7O=EWc) / 8ra37aK2op邾sp[#oIJ7Ӷǜ1A?yvH{ۗ-4㧝eۧ`bO?(R_.1 Wi` z9 :{XSP>sV 0q02vލwd3~ F{cI+oRS?-,>9slH" ˖q"AL=.Uu6nH8 vkgH-6[?(~Tk{7`jTǟ#G\^ ㄙϣ,gIWWO-_Ț:{zN4Z(O䫎RĻA ; 9{ɫa+Q{z[ˮ!s\{d)nۂj׌k鍁Xph3av8$Єx;,ȷ~FON pbi%GHxi9 !LO~5~dxtb؍U }H'}Rs6+dNtf'$`>J8z1 nႳN@>ߟ"n7Ϣ\C<\AiVu׻F ]ݐ ߹f6tLdBzwඇ7:؉Ǧ/uoӯkWom?䚧zQ0↽$SuB9Fңn_LX]}e'q/YJ 0"m[l+SuU :!УO>'6tFEhd>^>irj_P}˥g!jwcF,ĐG!gj r'Hw;Ĕ MWu(]7xx|>MX*B/Mv<4{. io$gϒ7$2[M郉oG~Tu5*;Ǯzz䫗2N:!4n]lkk|(:\ iæʞ/ׯgt;h[Ƴl~ݦsxTkٴF _!G!-xn^\/[E% 7~d@ǧ_L; @r~t8N12t+*.T6άj,ٮ$6dp88}50遑zcT}ʞv>Ry.0؜y^] W#{Ets>D\ˎϤ}+:^| 95ٝ"W۠6XۓkT!s@ qW /j\rᾱxC?Nݏ{ԨHH |?R+db~Q=lWj$?E&9zʥSn5 Dn[[d5a#jPGx?`=nӯ)CFBf].˳oOr-ŕ/aLl\H.F:yFŸv2fj&ֈzkAJ]kzI+W׾܇(T89rvv_jm0~귥ZϣﴗrT|9>_{Gfq1Es,M2#ķ,OEN YR_\nt38&vϝ֯<JL? R=nEE a;[73ߺHѴz?]iaw=([4:ww5t8 kjFZ]^Y-+ĉ3d-~\?\&nyU]Y"4N o0~>tꍡc1^$/^UI^׏r8xUI9Ya{yGUnÜA#1qPA4 PNkݸEt6jXC#xp[%~|}vńQ GE33OǏl IDATشn%  %Py:!;M@^)}FJiVX=?6' ]Q[Vf^V4g˶'zgb0}rc(nCz4iؼI%]/A41ژ€=ih 9, AbiX1KMXF&`]zԬ[#U`⽈nⵀ)G?.S}J7*'t&džM}>46JU:cIsj]j]M_f##=0W s_,CpTHN m7ދKbOWaНףKNx^Cv)NyW))NE'aMx<{^"uL` J䷼Gh|08r[׮Łcx*%o3oeR:mM =4Lbu!|6W4 +7 E9ڵJ̟ )GASb/nKib1+<-`%Gz`?˪kQ1 ܰUnZV1: ;u/R_.PUNרlΛd̟ i$\]q 87$HHܡy?:-:H⮝Ԁ˗Ȏ<孥<|W>8pYHsv$ߘg[$&&jofk2Dn^A ZiJ\xZ` lmv dd~ur;?Vԃ+4ѕa2opqtg')I`:B/8;g!79A8٫q ,|;v! L|碷&O5_| HI`Y|^znoVߥLy̜J On8COMñ'q܁W|j`ݭJ6|(ezOA0-0>N{OEQ+˟ScuZOSsrP.cZГb_-/M$vIOGNdIE׮܇M8 $c2T, 6hs]kSOuW]H&:v7ʈ>C#oa[䧸2nCQ8"Gȑl~+7 0paE|M&E[h9I g~J#!l6DS< VٹhUM?D/"!vBs 'hOhn 餤8?"GyIgW|қױU{{N8e {*wJv<qԈ/10 Շ\=/= c*q!#Zr/sc1q47wR+bLZ_hM@XޒexD)$$Ui0A 7n&][A3iEoRצ!'[ 56##5ڏE둛+8אth %_)J"dpnْ ˝8Q]َߟvO55їcQN=eɯY>ݴ’KQ! yE%[izGVEU+ :1g`&ws5fY5.IŰ 4LY?'J lb~84q2x s; _[(~`1 Qh~ t7МemeN#tnu~|]S! X) UFqDaz )c@Е&涭ӌNnA~Q$$$$J9L\KjN'lK@!Ĭ]zX 0cA\z;:qr]>ɶUY`6^EG혨h&I+`uბ++ێ8V%}X\~ij(Xn56nwb*>Cp/ֶd ??|$$$$J0#}.$?䋌xt117,$f0.K@&ի4 y;.B"$S͌?2Mve5%S ó䏅wʼn8Q3+_=Pt}۶ ex5 ??ȑ333!ʹ;U>?Vnzi] Gmm-jjjP]]+-OK\uA.6Ƿ,NhkT%_S4Oո|7rJO#ARxXHpnLk݇?oFϷ]l{b2m夐(lAc] 屰䷱cz J %1K-9i8}xWTȷy_SO썒̍5G)`N9]1,93s~"pY' O)T:BNhy;RM . 犱\fAcidwI`؃hXA/ʫ+ĕeOgGh)p @w3x0Nv~>P"ݦeeeE? NܦF Nċ:pj[Pet Z I_LË=>i9=[^1x.4\$"f:Lo_S=6#}1hJSIĔ\)c/5q *L6B"E P_ioY1eE2GXQ [2zMFnJ#.]C/2DCGwc>}d&LM?(@意Ƒ1yS&iFmcb H Aqk5iReX8O'oh^Y2+lLS jy cc BN^HQ'r;eƎk1ۜ.劰l3<o s8%%Ez0Rb4~()q: c=}3rH!ߘe0kͽ[٤vĢTR*+i]57$uāu.1ѩ3.A~" If[5U    lkإsxl6 ȕT?v+O7 (UՌ<<׼',;`P6*v7G}}D:y xuj,g:PػD9{«V[!NEf ut~*&˅'S,|4FOP͕b Q-WW F6)9_gSE93`aM /J\%[xz@4=:~_ZC/B/ 'X'momo?+ۤw`9Jx>(fPRkg"mMWkc(KU|BE&tvcLTn3w"\ m E8mbDnh    7% JЫXuN0 !< ʻ*ʺF5%vw=t^cg>R&ÑaULjaW@C]Qv 8VqMa (_m471vG`@GL/m\)vF r2]ϲ-ҒuJZOeF7`}pnW^Ji\ViLf=??tmw>c16~W] - w0M-`ErC>Br|>bCl-fS S{Zð=ZVg%(6iiq!ՆYPlN6a7aZL@@@!o(Gfa\"7JtH6[wa@nz*Y}{ٹU Q}Q@ gWcCf!4*8_Ub ?J8MIf_!i+̏q߁Z Q59`@XA$/ٌ(K64FqPl8 o9G*i4Gb?Vs~+ Q CZfY*nH.~KM }(?/MQ[?o8jZJ]ڌ_8mBE"z` kGC'~ m؀0hAIE;H H H H`I`NYY5Y)}3v9is3āQu!;5s[0/˻yq!PөK7#S0B/ch4ٗy>AMHc9]/W\x=8ŕe2sEsTeYs(Ԡ(,;]Ofk=0&е/ 6Z)8ՔF< e+Z(+СC5mmvt2-h=HjzK7EJ7EHGbZi_ڴn~WpH}ؘ5pYx{CYl@L=tc7c86FDe s5 U =8sp`I9xpMU &H H H H}%0{e=^ ccFا8 pS2 f?h@Z%$ g(߃3qb~8wFmLK&䊯d{A]!yx[yca\N|fe ]OWOvS4_ 攉@%0GJh IlezѻaV >%N;t޶ t>3G;bN.PFJn>*@.J$57,_U #` ]:#ZZd7>Z겉)XUB+d[!PiDh Ji%E]( ?zJӮȒo-.EAIppp,L@@@@HUxsjַK=]}/ntA'3V`iؽ,ch/\U;f}2MZ݈ d#To}ftJ|GAub2쀩~b!LA'z< pAn 7 h-M@V]`Y-pK7s?kJjߪg5t[p @^ܶ1~[(J&W${ZALqt'-A BZzin-r"P v#_$ѭeA1_b6+ aÌ#`v]XI';yssHD`ޯƒ/AuQ&s@`IQlk   dGgczeFvUw{'>?Kl9~IJ !^;7;夓 ?7w,B Vg}RS"TRV>ۭ >Pl5Zs.oGa!G^C˺(0?mpY6;|`Wa| kZOab:}c}[Xݚ)wn|wo9{7umUhK>Sp4p- =5/G6^юrxg4Tb v줥cG'&m;ޣ7wk ?^e\=qcc+)@Jz[3 Da]?XzoPtηQtQHPL>kO9($$$$ 0}ff5d65%5ad"˽GeJa  |?vafOv-LI~Ux묉'?’/K?٪);x˹`y?X,I NIM.=<˃4 (4n=ֹyޯd͸]9֤*9cp&h&e\̋oV3` :Iϓ{mۊkso5hҭM.Zm_F=!kd1xV"gMu>Nan.Q.4\!9jKoa>>OuZXSh-FڄG{\;\y~v02Y]g/;q=?<Β4=4 y=rfΔTѽW314y*/ \ڹzH'+mNSث zEכEċPtw 0vr -%E珖ˑwAa{(3Mi4ʅRם{: 镎?XD'/^v-o^Kƹ%~Z|M%̀&"/teVL+,#X;J?xU~d+ǚ9#|mꖌ4?!%XFJ0gD0>Ft̟iN~t#Gj:Hئ5=pJKSpW5n&N\k܁m+:mz`b %%qbk4b+Kt8o`d{/rT IDATkʷCXx1mߌjK>7eA5Wujfax, ;|hyyno۪_D+'ŧP<`(3E{I{*?GoU/ގ ϸǝ$8+FUcNx8᠟v'7- 5H`:K蔋 :,7Cñ'CGQi2ǡQ.f9&_9ԭa:L8K>뿗Ǣ  .ŸX0e H1A] Y F-dwLnbfuWN1ѕ):]k1Zw^zEnaq+2;`N}X 1%8>*-Ⱦ)uoţ=B௥8ԶyEѩ(umZ0 hb@r+@H`֬Y(+ rOcTЦ6[PIFo@켵18=ͧF ˻ch8SQ]CqԵ/׿vKYM^xaK4/a~=I~4"ƪ%x<Ecw|7! ٘Wk~ j|k+su.9 kZN: (4ie%>x MdۜXџۥ;68?/df a[yȋ;nEiM WKk0}e^7cqTaaFё۠Km\?m1n7Vz!t|>Ta.Y!=M p/~g|UM ݊[ba̍DYNue};2U[/<#hQxOӕ#~uX,]aFN/̟QB+'89:̼ +׿{챇 !ܑŸME-(>fQK`;JN<eEslleC9J@76?`VЇ[6߂|IJY'nEmM-:{8t@G ZY)QPCE4_N{7]6Y}ȩz.~uB4׬:YDioJh]ኯ_VcVXfyV@^:s9޻c ke&Lhh\D9V/LuǏ{EEEt_3fਣM+=֥ܶ?FQӖi AhԖΔBWÎ6:M/~uO.=uLB*8S\SE&eCID 3K,9iKhd '!\82L$ˁ.vGqf]3jg? 8[_kl|c(S瞺 Qy{?azcZ'OPz"%?jNֱ}2k/:9ZFh9i/#xj+c/cݷ!ؓs^AOX\L[ƕ3q;H H H$^^>[]+v끌T9)Z*S#[v=1{haf3̇n9Yl:#9nȔ [n#?0 Fodn\&@tWt "@]bnvrr%7>7.K >CEL9ѐL `%e!q+ĉis73bI3?z?EcI@zL1|a^iz8}ڗKP\\|#F@>};=uT|[o="Nlcutkџ-x< f]cFMHk3v27~|%%^Ꚓ΅ViV#4\(JyxPĉ[ѩ'+zíhN1?8s >z*~: ͰQ:iiAdZ&L8եGɶ- (LOmFrS߳/RsrN2TI}rvI Sg~ yGg^NҸ ؑ$.n#eeHţ{W+W rL+ݬ!RVni& SyiV15Sq:ЕVfw#?-6G=ZM"ꐤO(`tCP0+du M eS]AZVƙ!L=|.W\]Ye^\f2Xőew.E-SEO/3]]%{VJ~.0u%YiJqsRk DW\,0։E胏u,;lkg4ob)pҙ{+W #fi`n牻w~}U\ѷԵ>-*4bӭMXxOdžmh9~kTf_~V½xlZWw/ay :˻듯/EwJ9t3)éFzn0L y/r+#1{ 4fjvR[!d]?_#[/hsPD[&KvNHxC[{Ϙa>iyvyȚ33?sU_~Z=|o.MC;vp N$ZƹN?ƾ=du8]K#^ea_ʾNܺKbi8%>W<&yH=3 yڗ9mDυ1) LzK\K\C1?FүÎ8 J$3͛o (dŸo9 \;xz Ȕdק>݋ڭz @8w]ô$X h4Gc[kezmJQCnm5NzH3aY>MC,ک.$óX\qvexzm> h]0Rr xLiPNtWP|#Ɲ``{5x@V***B~~>rss }cnzW|~_UUJr!hDD8 yü[dQ8ayEtI=;%%%ڮɼ<횯,r&o(džq咫j2D!T:ضo$AWz߬Rff+_?֛ .\]qRfپ#\Rȝj[op7geCZDbSv+kT:-r Ѿd!^ 3tty7t6K|C Nf%(mi0,  \B%`9PJ&u':elL4!8 k0fc+0Fhw1GxMiiCM1a<#;- Ws56œuYXpn>&*+yfj0^J%\OՎs_)[9A$V=pu$pԚcVmL'| 1G@ك` RKoA$ fGϠ*ۚng~_\Lon@ -7QCa-WhN G:ҋ:ȯXLxLtrOi0uOX&jqF\<Ԓ|.27|O&IN$+ĥCvƒO&a"7/cX#0Jm*p .<|ӫzh)/)M!/9,SD 90ʾQ偹/4d_-. zUKC4eq)e 7~sB鐟oODqB] Z*K1&= 49x.caI+O.Ŷb;˜V! p̕Wtde _OØq2`EJ FZ/Mt+`/YtcՒM 6uQNx/DťCFb'aUbV$:cl[lBK@@fJv?\/Vޝ]$'EۖhW˔kB(f.JsYiGIFc``|uw YF6E  lQ 麢9K> +`mC$`Ovۤt˥B/fs~+D.]qqGn zi}XaSZO/4\f.Փyb4#'6^'snvm8RFbQ% ƍ_tT2^ ܴ(û+[?j-S0Uc4bٌ3ف?o#г{(~TM59bqСaZC u{(~3;y=i?nEfy,(Q5q+`jI)~Ǖblo sihx 4nuܞ_턢~P9s̻XE9A#"3Nq|L%H H H`=j_Tg~#S+qxNH<|Cؗ)džNܜ)yjFbg(. Э.NA P:m|~7 K!S &-klFAF2z7,7dž"Z+|B|jiD,^@N8匓C;KgjY̗ղmcA n1ƫ?43ތY< 6%$+Fcme??/}|䚟*mX0Zo`Kr<3m vPbq)OyxZ&Ͻ[ޑnROw}*RNXzcڶ/ *$MgNgo>OÐglYNsZjrma]crvn'vH[q["noco/1EgET92$;H H`S$07OZ~T.8ANΖn}Pe664o4`BHZunϯJI/>5QƀR suj׵>euWيZ|g-B| i+0EәJV7bvU=\?~<&9h8"%Rsk4Q ƊOY0إPӊ&W{XWci9O<$sO!; Cy>?̉ocl,ԿXۗ()(O%^ IT?wL\z:?r (<\R X98`Y'`5`  8#ON>= 1INtEܽgVZm vX99\;F N1g2n'JOwgUve8R*e9MANz6>z;p/$9w&{48T=w-ے/qGoI?;Gʻ~V5ʛHY~].8av7'Z!&9uRe͕KWo.uN:6,1GБ>}w` m;B_iХ];>7SOY%" {mZ#q?o3gfc >>(עW/ۨ  ӕWsd,RKoUCNũ*' dJ054cNf8w$N֌I\49W(#@(sW,j.Ƽgė# N:%'| < [`6 I`Uxcj|w1V#ܳR ,>c?% Bwyum6 !?K8HA"~|F,>^ĥƯ|@Em#.[eb8_NBhv_&Wп |pj)X#ϖ u][wegv`8wV:+JPvQDD=[P] M۟n+WLwOŅ_J.A 2 gff4>]]]֢N>ՠ홆wUx p Խ, =ġ}1.8U+-:Rk#[8} TP] 5/=Oݏ^Q0`(ҳ;_|bbʶׅPg&\ v &&(^7ѓ5ZdD/P8˗a/; ég#D; 4xB5u q0w+Q  l<=O\û*$ :u@P:r`D&묵;ƣODGտBN>(8\V^D&q,.s*լMFtG^1q']R$]>P?'rсHy5Mq9dr&?p<8;} lW'fLLma>$賳!T!?+ɶ%عk!%tMtֻג=[A1 -^ T|0-gtu__C;dೊy/#/em~WY6=}{l<O~K.w܁_\s6`Б1З y߅Ʈ~p`\V)iօ_\:  $hs^SH*q@lH96\q=.q@`% lhZ⶞:L ~lE#qqE8~SrÐ/xu;4e.:̉[L@9}RWdIYWWH6,g@Zǩ42Zi3me}ߘ5j]y`;|6f9y!KYٙA 1p3q;H H`۔ՍG ꄎ9\iq[؉aڟHPM>6}\uICGoC\0)[F~dIxgT8W~2XN&NEM1)yT8wj$6%x3qD;;p7F^t۽3zEV"3/CxݓEĨ>\Yow7g pb| 28܉a 70qB~yh mbbsa9׵yƖ`b3|h Fq鵭禟4jpss4$D?ņ_%fa'FTiPX-Y)0(,%O0'sf#UMRp5u2"qbi%q; Q`,~l4nd۱V|uFłB~o#{֏5`mMAA۪V7b>'@,ºsjhݰoiTvO9,z 7ҹDiXM:4" H Ur-s}KiϽ qnp@Dp)V6#o^b[{+xmC?.KQҧy=QܫPV3RmtL 䠰U-t/W%[WȮF I*~Hf8W҇cfmNp+aaf3~Wu 0Ґƿ-RbMiMԗ&je܋UWtd˧sr ? ~gSێBI~r@A#0s SsΣ< 2k-,Ak 馾Q-ڦ6'?6ִ D6t8Si:F84uJ;sdˣ1~FhZO{^' '4V74Iџw_fo .I ~[ޫw+mcO/ mE,8_1?a+8`7m.ۗkofۼǟ ,S/#vIJ:XFI̍|6zj8F i4.elHL>1{G 5&d3|ȦoqXӨ{>efdpdw"&l:?>rYT($fW}Yj}#&')Pp)84Ly~/ h4x(j_~ iSޗȑC^ j4 fъu UeidSVspu1DTt&ɷG;NFaɔH%vMK9ά?1> 6xGe &H H`KIŊ+$9)% ԵI6dߧU_݈\\¢!w럎[p@阼8p B-MPr*2ݮlt56 O tE8eP` dp>``9aްm7QLn0lSMbaCyaa|EFFX֪?hÆc5$ҰC?mոQ1<>oE݅T=zIñT9ՔU\茶sY6(ۄX~rv弩br,+ye8r3eVR H֕n׬+RCGXGXpŷTN.w eVn$ -#UL.NJf7# x%- lֶH>r@*W{{H0{ =avt 푁{'׺>H`'~¥Lra}ƹiE e(ضUAK@FKs#+c2? [m~iS4Fpi@b-4 "HbHި!FFkP։[N4٩m+ؖ}BPkXНHQ`< 9}В"J*餂d)i0_ (ş.g矕Rg?o$%-ƕGGJdAcK߁bت|8h>~y%HqjY|X.AA%rՀ}rB>=ѧr.A0u~̾؇8LJǓ+S9 -2oV)⫖cvO>k&5{ZIG&@%ex2\Y6uj|d'❋qΐbdg,\0b Γmxu-.A-tiY]_|bL$_sPjd dQ2~ ?Z60'a :L:e]9ӜzY1^6 ^,i9yDWVp3RNzeELYZ ? NY+[[np b4+'M%)G "0^0CTz(s?P0_~y%[y29PLs v" MWwñU6eQAgXVd/(#[B+RRqd,{H7F xlZ ^Sث{ ^\vpi,ɁmtKE_/MP9KMzES  l>ZR>\{UOaNh A_N<(S*CQ\<A Y .JiŲ{dQ3v&O'i/hzв}3O<޻G*^ͪIAE$`ik OLE~e8O.xavw0\v< +nǓT .3~/hiM_J~da~/ ?7EH1=2)z?&4?Ao[`lli#Ga4L".T܂ H1Gc28;#~n8(i@1\M1=mqۀ1mcL-0o;Zl (n8 %yx>"}"ew?]-G!o چfs"vU!ożq{~3iEx#*;(jxG*l3ZzbÜ^@cq8$>Nń 0 |/Y1iY\* ,_)3h]/W0< NClأ\EW5 E2ݳ[lM eVMϭMwi2.)lb|E`_IE$+} Ӑւyy8{h9:;     |%>#<_9/Othu-t:bs6Yu '0p$WTh -fۊٺr<`'4~gh=O݇>t8E>k\TjI) WpQ;Ƶ?;kـZ 3QHN> Ouo{>nrP u;G,Ҽܪ,̝r ÎC}U<"κ(;աuRKQVQ<)о%XNΗak #~ jk'peC:7?F)wFץaY藝 xg|. sjS\4!]:V/'WGrivX;];t[Wclf̱( &H H H H`@~v=ї37=:R077X[?@O$; 06b 6abL:}xи`VwW;|f;x6ל|]i̘>=Ȳ/}W/߾_4H7|t\; &="Q#ÍX&:tǢgR,xa̾wvEvw&'T$I^VOzh6cl>z`gm?`,?@27 >JJY#4s!9j}W|hZ6p؀W3Mc;eNeոgpK'hڵvFs΢j}(j9\ IDAT- lg lW= E#90 p1g`C4\M53c4 \5ofdJvhr4N8އO5EL&$Ymٌ|u{<$j2.ǝK& + pMl,yi`ϓeu *o?Z{4{pr+Z}cq2;~ {oz#v_x)K:؂$?JfTtLٽKΡ5JJX u{~]b=c癲oV.xgN9Sd[Ã砿?#۳Y]%K;SN]yXiZ}<$Psi|6 trM  X6 GhH Z}D -f ӑف_%'b넸uǰ r~@3̶Hۀ.o'`ճ [O .[ ^vWTKU   1#7w}}Q!HZ{%Sp4P-0G%+[v?DuLrQX}BQcFOEo u0A_ ̬-U4'"/ÝN0gXwVHVݏ׼A1ē3wFi  |SsE5xpA 떃w#s ~Yk!Lպp:LV*& q     $@Nyqo#c~wG6f .՟ sJk)&Y&nxR+}.h84}|tG(,-q+:ev<XPͨyEJ~=o7܀]Bɿc&`\V |:xo]u8rl)rp~ Uf3miX}PG-՜sYV/KG=@g9zѤw^g;(;G/"" Xb5I4FM4DcL1%`Q;ؐ"(]Q{w܉;wߝ7y};3;O/ݚhDkL߁1}c]JݬמryT>T=]ܖX˜<\ٵC6}vMF)?6N~yi<it~\rw4gnG?lr $H,X @bh@L@?{ZGaS-pgY"F6Τ˨LavrǙjLFk!ﰫo9;4_blr4S~qa`1~~)n `_fntuwYDxH){?)2)K۔XͪNKyetbYVo^X1hF`HsVX#?%\_|e<>3mFYN)][J?g;|X%H,X @bY` ^V&o~Z.Koܼ[s>°sxi.>y~?g/hb=juAEt>;L>;>S_Rt,=(Y{%Q컋=ۥp Rvٿ r}oG)0+'Ɂ{)^|TfU͚$A~I΢2fPMu<%cΑCFJAɢ';G/-!+)4Ĕ$_ʳ+St ŨmU[<*A}m %U5-bYsy0 _^|~#C2e~łdujGkd ftU$Qi8P_1_X|j䌁r"0 %H,X @bbqv-',NA:9<*aY~?MY"&)xݍn..4cYu fk8\^,|. KZb󭂎]5/nŝ1݀AЦ6w[r g{\Dɽ+':se܅`/ 򃞭dž˄yMQ[( 8i|(n?xwawݓ dH-YrH,,6UR;ȗw,ma|Ͳr֗ ^=Ӄ# ñ$7vr z|pYwi)sZdcrLXĖ G,kwry%P3NT Hc7BXl>Ʋy#P][&[m>-Q4X @b |R88?^|ww<7q\T8: yQbPuX_Birf 7ܠ3^GPi& m 3˪0YZ.>p ~;%XJ=X ޿L2j8dGi{ }w#?&p%;+G^jo}Bb{6ۧ{=@< Jtf[tO` i;h,]̓$r}Dy+%='Wa%XV~}2O&b9USt1>ę4VomGjB%xޥs9e1@+ŲePd xWx^ dJyeA]UU-3?\R処keAH YkʵSm$?+jX @b[ }O r/.@^S)Fv\(|/KNLpqZ߀L|=[i;7ykJyrJrEIVt0_7Ss 1*$cY 3L{RcQʚ Ȣ&axqOlŹ-" kAm"ua"Sr 6˓+?)},s>n4s6ئl6z97Kѱg˝ESm-;l9&z`ii!YmIمjmqڐˤ_4Q["u5d62o[\cu'M:|̀u5ۓF`٘"M~se06>woZY/]rvo!+nErؐb9L ['MX-ԡm-Qa2Wa KLlJ^/o/L $H,XIr 6q;VWW˪UrJYl-Z$=VT,\"b |CRiն9XСꯤDZh!|`/yyy $nof:EcrpL`̟Ӹ4Iם3vWϟd/U`\Xo-2j==FpKQt~2x2*y 3Žɇw{?dgviM5 M>M>|>h~Q/`rX|,=t t+,a26ʩJ60Iۓr ͔NؿX7c6Ȟu2cE.)D[@3ddVE:^\ m̬ P klo/̒*߾mn-W!Ϫn͙h3VBe|>?C+\!g%q lxe˖:&xNp,ϗ|3&H,-ŀu׬Y#WV@\YY)+V˥BHgRƁJtd:koql,U[TTX͏4 l yg[ x{[-NϕExpXo03:̜,OvsT{|6%ϏY1%rUG,n3Ɗ^w ـw]OZ2C3AցK'J渵K=nL2祠wQ𖗗k'9@/É۸^Jv |9ˢ 5?j&έAhlWruL:r 2򣑅Rl\Dd3Eke>^;0H/V.+PZZ u=vtG-'{{ 8 %qj N:#$h^~S}}aO5asFx6O/(aH,`#`|& i{=,5yN00,/<2\jsGK9f=~@)_2Z[L709q{?T}x+/#>]f /,ݴ)Sեztnb> Kx!Uu43+)%%{9d Jt84ݜK0(m; m,=x/z|H+id3%7/ \<)l5Ajϓ^K{ >\;m/'ῼL~b٭c\MӴDV^^Nk'CD~hw=k[ΨfxGxߧXnܷL#Gc p4ay僩Se挙RJr r%}Cnab/*6jAY?~˨d=ԇ۱ɒS ,1Ca+(Vb/t=Ӛj-!fsg~|as>q@썩'`c ],O 3Ǿ<2־:yە}ƟKٮ |tuh 4EKS%O(ɃښYJp/' M o/JGKѣ(ϊ o-}|2\f\.o6OfL[(C"sr70 EղK2yi̭9Onp85Q'f 2jH`803HS:|8VN$j:lulty</崮#CFmY3_WrnBiJ(Y`ԩr;i|=ޑ}IG iգDZu/:j pPOB*U˫e)+e2k)MŷO:$}}vg;! !<>>Oy419$h >0 ixSl fc8(&&Ks0zoHM)|"c[T %(v` 3NL>( 髶LclW 1iN-.3P2s1Aa خ6زO>zJF>̓.qD;d]wJIIqL_G8ً`]e= ~+ tO0W^u<Ҿ]7sʷV׬>\&7S.Kmw|J̝;WAmc# Я DmTG[G-ƇZ=3SW:qщ'[kjȂwɧo̗)Δ57zʩ2tи*L&x.?sLq|A:{:3쾇 459V'Wx=<.ۈ4  rwv6`^iHbͰ+}18b>+L:lHlTmko oo~baI澶sdc%פYXK;s&) A<-3?dl3N&,.qFiimBFu:7dY{?.gP2[kzme>|>Lٯ>'wIz_{O<\s͕2{L9bL?Hlf:1ٹŇi0&="3κuu2a߇>H9d2Ώc Bz$v 4/}_8"u\ G 8ȱDbt?ͬ xƼO,#"L,M{|lD!{;L>lߗZƍ';[CFt,8z6p0-JL6"uxRyK.W\.lpw`KIs0 ۢ@qpvD˴ c۞b~P?|v%|6?lҺj[5:tvu>S6Y.}Ϥ(kʔJC4䡠;bh< u^kV,S`KTEV!}ড়8&[>z|wv,ycǎ_rZar;>Y ECauԆLXRyln;yժrۿ'ʭ7%C +Fv}w58r6+wiTe4rMeFFL<}0+Lh'9Q=k:.ikg*ҡ:&DڕC]ANe 9H? /&3v{ G`ˌsأn:#biġ Ol&f}TJκ|W`e7cw2č>w +_.wv u meL/ [7&f;w ͜9KyY9?;S} X2e/5#g:y|@>eX`K-J}^{xC 2ۺ6BI IDAT~XvLcۚӽ ~h\轒w 0$مZ Tep v{g+ w{"y׌Ϩu^q|k+VsMd:ܠw>oʠe&uR^kV;gW.^8R߀QGf~ (t(#qZ:]eC.4]Vy=o_: OSom:6Z%w~\zaVʱj`Ҵ=-3IdP%/`)OJ#˔ص J i!zai˪7{SK%n?9yO=./Mv9>gyoCzivYTa&~G *3x@z OVm`p;>'qXVTF''OO>:`t+aq}]ʪ y~ TL8I<0yGh/s,}U/4 SroH>Ht64yGO, +:1q=y϶oT{[rWcxC cSuO2'y[ƻHNqxoc؀`Zע$H@rZ)@1ߗccjfu%J)hjY:v,}?Y?%wϗzPοxO99> 3aׇ`WObvERBY Xd)@ +Gi\U%5rſ .0K |x⍂a1}-:4~Ss7 =$9>.ym:poß/o4Ns2R`&wB=fy'tAjЧ~4==dBYSBQI~[d$GƏ/'x|2AZ,,|& Z_>*(A|6u2Te?_Zf 0̏U>,]*w}t1VM?fU~J%2—D~ ^q)yMYXWK:6QX=]b|ސ~&@gE>z\{YybLԿ%Yұ ]>ZV-gMwZ[`rIGJ^m/8eљ`b-q?9aU{1 WD_db 腈H]K,g8}G8微%l8N#8; &jɲl߃e3Vzۺ]?f leu~N}>cP?,3}p$'? 0n?i]T,hkKJlO;wo\{kz\;\S|* Qfy'Ȅ)|lMdrCR=-w/ӫr-afb[YwC٩uʙU(ʲp5B3L5yYfL4y }4M;|OcdT6.Z+SVʵ~=bK;q9SewvC~rXX mK}f.A2ۧb.>CgOWu֮Ѹ,T^cWS0[7R{^5Z>ktr?K.$wFjal/sSPB'͊Nu.չ;#kUD>ߖ՟] O?T?+Ǹ?OUƅ3+v:ii:@4wO, 耿#yӔa;9 >iަ)80v L9v;ʱVF3Wկ>#q3z" l YZjyÌY @7+y= PqSctHֳߡM#ʥI˲cWVV1)+*fˣOG:tRr C^lauh<ܝA=Ɩni(MK9kd tp|-{"?8t9[g`gœںrcse2C7f}Vu)]HzڮWPliv21q 6!r 4 J4$ t:{@1,篪v3K+id;˿o1֝P`ug5Zi sfZ}VF@iҭ28B[ņZ Y_:j8ܛeiB_ ә{ 1s ︋I8@c,ulCJיsS| 0&Ӽ]R7?ٽv_#LS'K^c gؼ !Sq k6sDe$a$H0aU&Y6 dոy c-{Sp#Rq/_ )_ug}?_(9Zl qw<\S tU4W3jNfb |gXBz˧iZ6;'#\ݢ⪛l#3Vb}:ΕkepRٳkaU,VȐe Zǂ3te2]GӰ=hQk'(/j]S'2].69u@9{QtEv <3wx),6>vf˳g~Ylt7X9it7(j/f-_xgԿ*<O`_Rj-_~eGӿzUF=BymzC脓@Y p1xoN$ex!hqkL D`#NGCHǿhD@2ǵ6&c#\yn0Jk''H,i x{S6 /~1VUud#ɬ]iNߩLX¼a6A:xzc#9@rsh3afk@g͵l#Yi(矙_ :gQ=9̞=[F%GK~z^(/GjJe^z- _#t~+i9+)zf!B/ 8[,it$?<9ΓzLӖg\+\2i-k#%O!Es\O? -aטA|'7}!棺4d=O,뚥`s:tͱ#[Iv,A-G9BT͒S?J $=!gci>_;G\l}#6o.GPTzkF5^S|k!4cc7lz<(AW)/a c|0)*JYl8>v^oYiYߢ44qSy):;k;֓~57-qr~~#~!u˅\kdg`It9kVEAHgg0?/o&)݁Ki |R)HVz{x'^l,#[#134ad 6dZm8h@pPq:A֞M㴍],8i '; 7oӦ(Id $v-@Nq` ̰bmsf"R_!fHQY_I˴YҀSMP(Ј䴘HKUƫ8`Rj" Ȁr@_Q~D- oUTT;GK~rh!`u0lDG&4ƵL'}e=Sq>#"=H? Û3ba+1A?G]p|;O~~lu[B#RnN/[%w,>EXVe QÉQis] ǵv*@0uiVf `YuL)__sjž1 8{g̕O?YhEi["ͶV; e:MTR_*t:M訡*ÿ4;=~$(`/i|؁:7H?{s~+<>Y>MnNG33ozPP_c|7C+F$׀e-YePlx}5]# Kb__ X31r5A00þ9A }R'ץE=IgqS#I#XŀPWRY\n~J ˩!^e鼁3p,Rҭ,玍bG.#n6KPN̙b(,v LP„OwiT:Ls`qdAg11=g8S\?w޲I&Aoo\~erW+ ?wWI+eHr֘Al0L8geĀ efkײSn Y8(MuH?X!(5TEFNߩDn;$ 6& Y/{ANeҾę@2|=a`k9! ]!#ƤQmj.SU&x B>DL&O/U+k֍b \ЏxӢjӨ^!a1_5xRY`!Kv CwjR:k'#9H|krUJU:u/":m\ŸX  g?c I;ƍu=!#R>qR=HFv~(M!Oa_䔇?p j Zjg*@Mܖd}@X1#s?|dlӹȪyN=8c[aκ 34}< q +0tx0O~ 1h R@ۅen-3P<7/W~r7n;@r{cɒStnv=E4љ]9 thVxpPYQp~ّ1N>=1$Qe&J4r`h|&0+]vi#+d-$'SP8q[ॗ^'J 0,-Hd2f@= :Tmba*@`whٌfkTs}i(|#,EqH8-_]zg۱  :͛!Vhyш-txfAhkˏ8hYlH.] ,o1B87}öxl^#|rQb#>*k${–X b@!0&n qC{e^Y-o-c휀a/qI:QCӓMH0 13-Ov:HiihY:Oy:=o~-궟jsu/neo9 k f~P.}>%TJFWJ<0X*ug+]>Oz-yG260Ra ~fz1(xGo&aI0B X`cq%C9cŔϬ/2r^UuP59J|o}))Z+%Y1pCP5vPP6x'Q d''QV&|0hjC|3 {h\IotKvv[%YR~娛יQΎjU6JJjE@i|ڬ-sv0:˙W6=JSu iSlhY=ڿ5b>Vd0-KF)uia?{~tذa. h?>O):g+3pCƝaӛ&)qI8f.^d):5nx8u:~9˕gtg $H,9 8(\B 9mK #ޜ&%8P<,~eY (,0Y^ޘo:k4dFLKk/Yx?K[<۷ct?8ИQ:;dѥќU|4[6: 2tg"y> +nEKN{!6xH'{d;;S5~Mc%-JuN:#ˡo>].w~DzgʓǔɁe8o^%r ˤ7:էP7 9Cv^u5}8192y{W ?է9ZQ?qOiy;y}x\+,l2͞L+(ʗ}.]B?斛:Y@;dVG/^5˖bƛ&I0;kRqYD|-MԓכH~βf\$=4@U8jlgHL4Sv~9U + 9xTer,j(굼i1,wA=i)nnƲO}UgW+صO3)]MCp駗*eEZ~AN].u^jT2KFwɑ*&ua퓥0Nh3fȿoN &Yqm[ c>qKԻnOySJ`9(!NY5@'f?-pc^ȱPLS!rF//RU9ծ- "펽HnRGuRi["AltEt#B]aFrʩ "s+`It¬OW&%9V~%.@sZ뮑!$E-zD؆. @ ,hQ:?$TF u #2pU0Ra 9 # 7 HOR pRe@dcE"%q LA >%U9Ƥ[w8<wl7 _DzTߜs9KSf:3@\$Gn -թ²>v-8qNˀP[HY+卅i][v.l1X9-g=+?|[Znht x@u=m$~S6|3C?@Y[FAwAA(If qw)ͳBŒYif3TvuVc.ByG娣J% 8 -Et˰y8)sT<( _{ps7 `\ׯ׿8-IwKd 4>)&Xm,biu/lq\ΤpJ[:<^@*~"yݺKN>^ٻM`1ܫhMFY A@YwptH0Ig>RI  Ai)9 "q$@Ff1++H!!YlժHDX\!J Qo< YI ]uo믿.#Fx_vKuJ)h |iW6Y]_[C=>gjH7EG?8Q>$?I@t]~=RL [S>*L@,fL$H,pA>x ;cb}4:^!fLЪIRb@,Y=6eH)}9~g\ SAJ'E p\ GvrL4 ͫ8T J!EE@HN_:mz9psa1әY`⠇~]j 5S`vIώ=;B- t-Ǭ(;Wʭu 79&؊x'}6Rfࡌ5P95k@ k-Gv:㤳{<=T&@c!f%\G3[ڡO p˟Uw:;1磾h4 HzwݾbQ Ġv;걡]'uUn ŮmuX[zq8CbK, Ny1U^,aʁ,4cdiy\;F)b[ά74tKyjMDf=#;o] ó%U>>նWex,>pgZ l2Ҥ>ԧ4ˣnҮ Wa[E}NjGhT0@AмPY j R&|<˰(t 1-][)R 0|WuXtEK .C#A۲ 7d(,3U1}1wˍ+> (up0쾃= k ôC_P.2PQ8Ϋ z /='OL#aoÙ-?*H,@ 9ogUHG1Faq`y2FWɼʵ2^淃X૴@EE逸'Kzɀ~ mUai0xc,`Na Z$PR8,Ҿ~~Q_|ϖ@evK $hF |?Hn2˚sxYWH&ҋou{-z{0:qz:P`: pU8(- NƗGSbsb*!+45OpAxv? A90tv+I޽sM}+Bx5v.ŷ˖~rX"(X`[/=':pfC!I.wٮh#VCe j $oԣfk[cb1j7$ې= @/+@ Yyt2 DsQY2gQ N&}fPMoߊtn2Gvb[2ͺŠN:OS0 YoZ]4'G)fcŸ'6 yp6i]Wro,Mf'&xM$H,МYLFU5|tX}dNMwK.s;pt ZttނmF(D4sg3+d:2r!?ywj: Dnj7ᨛfFa(W90&Agot0FČqp`L[c4#W=. )glKzW%t/Fl+ۺ}9C 4T~ X@m E}yNw]RfGca@hhQڻF pr,/&ݗFSv'ৗ5~mdYL۔+Xs}m C.{_~yץZTIz6f=L*OӽMFX7Nr6m>ޘjݸ-Eڠ,Mf&xM$H,М*)A?$E mSz1Y&g蘵g G7P4ń&Dbj@7!ˤ}vH)4sR*`a+@߇)4m 2ޛXh0^9K= Nɂk*2O);aaJvm+Nn>qe ,[L|2g62Om?k>ٌH}9Clܽ gi $yaX*<'^pQ"4ChtD r ˩L ϩq[:M Kf 3>=1imLON|7l.v49ȠzT!Ԍ":ið;~ڮ6\ssCݧu8hUi=: G[m~nOJ'OfR@EfK $hN TWO˰1 bh;0sCV87be8ԸҌg|-3$psQ^x߰wOmk,/ٹd׫.ܫb#7,0YYg4?tLtv !d2vY{' 3ddG Nt%-qFx7n]%##gEcSP' f6,&ݫ*)`O`6̜Hț`y`+UJꠟ^ ǗKBbBwA,>B?9I|Э^g'fg0ސ/fwk-MFADfJnI8M1:S]~V/8SlxNwCxB?=qC6i6FYlMyϳyX/< - $ؖ,0}2P$tٝAә\]g?8;̛B7b`?˱U:=M޲ C,#ˤqҸ4dI>VYS <ƩAfKr1P ͵;9Gjn-g̵-,:W ;DԴwm |>}\Y\〮kҌBJ`<_xwXSLd9j_b`Ze ˇ_l iE-t8Y-Y.u Xɇ)S>74>}Tj)9XX[Lyf1u;tSҝȈxyN|bMԟ,1SzPXX`NiaA78o~46 I So)v3uFmͥ2ѓX @bm0.~׆(?udł6< ,l _TaRiV!]P\.LF*Ƌ iqLQw>@V ZEuEV]gY=O,uiSEY; h ٞxB?E($kzq>!vJY^M--;le}!dpSwCQ>ecW$mJ)qpwZ/bZve=j趦|üOF4M0^"X og|"{*7}ezF5},9Xi[r)9\Z"Z+ 1+%;UW~pYu!|.Ɂ/gan9 =˻4ODN󷱀8H#gJ(HC q|UAxS 0FJLPZ^k I[V$Z44m0@Pi1/L1wO鶤ڗ\Y,M eP#٦ڵ(ί-ȵ&o R-{Ȭ韘'?-4 v48m9CNmsptGijM˺_mep֞cO̖ƒڊweq^XK#NS$e[ԩmBNN[wbۄ/e̮G٭,%!nf& $v,P^^.yyRP1~ŠrP?t._T .@?Ynm %&]:3B1i>lĆwI3 $: GxlAUNr3`^8šS牂"I2|$n ̙֭=I9Cbs̑FxCжͅ 4m"X+066ɇ˵ kOF ͕qW$?mTM~GNBw` !g|Gc:[ TI8p ~1:ɇn{ YFcKGS@X߁e珒ϐyo/͕?.vx$fԎ VU_釺Hz7uHfמ! TVVJ.:Q (%  s8{9L'({YF3Hɸ/S~,Wr*']A|kw9=$/24R޿^&Yy<63-=ZW"\?NV7[n8 3u|)؆ :]RSλQ[gɏ){cFu9r-w9j"|}S.'/C*+ q B}7ipF@ vIଭY]bɑzIiQ img9"+>,qWϖ<5Fp(B9ZqN%gfeWʕw2Uϗ~#?Hr{suCY΋^,t!l.j-;/HtO#79&5Β;<۔z7aG]7\vnOːFɻ*^-Y WIkw󠌝_9WeӪpH?d_%i?wp="'Xrݹ@,,ҋ"]foEFcOT`,??{k4Ʈ1#l(@E(,.Ϲw}+4(ov73wܙoy3o ?q=GMCB3⠿EN90|)_Ɵ*(e[ׯs *N)\)6սS?G'?#wiBnGco=mHrp/BS17?WzO_fv)NpOgc7;DB=vuqÝb0^XkaK6fԟjppٸoXR 4 @\Pǜr@_4p̉M m|HBp_w~ޜJ\{t/ _59C|惘tɱo8z͇F =o?v1 K'> '#o~s+-S7qߎ= wκ~|leOuĐȢaeN/~ XW'M6hVz9e=Rxdsָ@ G+gbr`ף>%x`zAḅ漆]cr0Ñ~o뀿aYJ3<9Ľ_-w1xw8n;B;Ň-sG`b ~c?8klt+< >GXDW?}cV Yar]1NhQ%z^]Xaư%ltb?]:6/bSNǭxbtGLxl ۧUn& l8GoBwJp๣ 0|@ JOcعcGa`3& xFq#o!}o\/렙Ie}.saQo#qyOcq BGݑgZ~s|p\|ƹhz}سzmƨ~[@Y`Xt,_G^ٛ->ͪw3 NEH5j|e4ՖGWj6gٞj: {ȉ|}zu=#8PNq>7p)C(xeJ,~= cKo]KA7a.دw=,Dջ)Ez'ۘ_RL~ipǙ{:Ne{[`ϛigNxw0qXmPN9zέc]2XD+I52iuڅiO-Ú0n[ |+jͲEZ'vod|6&"մtY2?~+0cqh~ 7l{$ޙK4FήL. O_}|ږq3/{~a:܆i+qe'/*Jknk%t[:[BayC*pH!I(qvcyrR* Ny?o;vqy8tb=W=9qɘ3Ou*a)]mx?_Y=jd5?װyzW=qNإMĻw;pzeMq"qs)?ainmtp 췈MYO{w~eĠ!סϨp^Ehqθ|^|<^.]s ޽ce󞞆kcy[ ij%zdӘ7Gڪb.g%9!(j @-MeeevHz'Qs޿|"ॴxa݋3N6_xI879_߉<0nL4din7]dm6LɓZz\VٴEx[#X^ >6kX|ȱd6>睆/{q^3~^b3-ʫc!/Yeym:S p5P`R/vMVzٰԶp#0֊j/'H)X`45j˹={c4NfbKB8V\iqyīy11r,1쮚,gL?UmZ[-m2)?x#-ux bZ*eȫC"oW k&;フ{_e<1y{pluG|ߊP-.\Iy( {&Gp9t yb]&#̞J]Lz5ɞ[YǵAe?w\Qt4o $~EJ߃-ɘzN]W"˭TpO?z2B#tC+-RYܬ,LQrk /yY„17˺ySyFXHiJȰtIs"5~$]6JJζFnma4.!(jd71;I]HoUcZ'/yg4p4 e^M%l&sވ˗a"~iD<pdwP%:x]:oSxw罎{+N=1+𾑉zqBy#n.;8jF^I#ƵmQJr bKQ>q(J&_qO xI.|z>}4y,ɊL|OǠnϋTfxbM|N 9|=TYvM")Aql8(Y5?7#5sXQ8'W515rm|?ɼ%X\RFcĕ}m|7*k >T m,U 4_7 EXr 7VsiTx ԩSKɅuqқm^ˋ[+9O1)Ige(=y\Sug?ixeNv9 )Pd ns.68z/ -}eDZITb,rn/So~25o8 bI;xJ=_5]b_'sϓ t+f9} J+1pxmA/&|,2 G{^&NCB~cYGeO om2,בnZ_nk\x-"~9a6:&]ҥK_QOW Z4&gJ̷?-g֐e vG9wm8(j v\4M6홣 ,d rnY3Q?C#sWnCc>B{'="3]Gwd,[A[+6 {OCu]kqKxS^x/;G3> G 'zgtQ|rot>̯; 8} v.<𪂲.tIzpi7?%<|+>m=%-9 +Ap|W8HW;J˖)ƦⳍMM5j e|֦jae[yJtm\$ɶ@;umfE9!<= ^ăSQf~]u!i%J.jT;i7"<+px='h[4U }~?iqqM/n֤8(CePjqw)>Å:uR@:J? )G6Ceq].]XO[Ac_"phFt#qpsbWG?/,= < =s 8O :䃮w`J,:tyع GtG+Kߟ㙳=ZwK"aX{P(;:O1e{ Kp} 6!A>&&MݏCtګ_s=h#6h DsCq9m{i;jq8!`Zۧl~v"B.q^a=„?jkr FM hgӣLv;$2j I+8`s;޷]lWTbbn[QGeRb^KKKѠA}|47VMf zX'Lᶿ,z|Oؾ$NoGK]`>LGu-|iSL.]85PQ!%vDwXRZuqQ!J>mҙ<ձ|A1JrдA=SԟL邹זPXk88]Q旡vQ΁W[.t1 |y'pK\5tbE00Jre9xf^EGjy]*`^ J*a [׫eTpje kf*c΢(WK;7Δ-̅ЫEkS_-nhڴ@_u`É 8AsP̗usk7`6<9z|認&-زǔbY(GS"Hd=C)$;jJD^+"vWM=E;g1<1,UW <3t<hԨy Pn]dVAsZ|ps_+ϙԝj @) ϘZ5"dW:Zg᠙6-e ItYZZ bhNQn g)+ e%׷__n{ڪ&5'rj*N>!p0/YEz9a]uOD~ e'EHMM:v9mVByS~Ѭmت1 dhj:a\ o':‘(&'&*0.`j)'~Xiz-4jRܕn=ټ<4lF9m,tw IDATc+Ö.NC-?'nQyZ&Vxɿ$+rZE0/Tgu*yӐw ю-0[aƳKLd#MN㻾R}>%ˀa&ꑍUKola>Ǹ0kJNs *d`Lm’a ŖJ4'ueq;x-/a2%_Ȱ\zy),YOF_wީb:ZEMWpb7nV%>5~ ܩ3z,.V1j'c]@8GxJn ҤSЭMiah]͸C`ZF}O@q%ӷ,Rŏ$e/Q2}]:eA&N `uGXcvU4~o ;sk$/ԗZk'ɧH,f4.?dk3Y) LLŤH5ejkמxoܧ;xu>`EWb׾K\)c據JJ5j zj웙]g[J9{^>_}"@1D $ 0ҭj3E 2("Ʉ$_v6VS<>+_%:fK SW,*kc _,C2@MtbcI8,̛ 3)ӳrSd(G0 5f'lm.ىjUfWw#]Z1JK~%8\)[yU̶dq 0$&IeEfj(p3nAW+ϲ"??kFϳ([0._laII ,Yb\zX]EV2cE1l |8^) P6aOP:q [+(+EI룁5/އu?o';$~vN0j &jhm s&|F]iѤ{,_RO>GA'W;oPs ^{xh7ʨ6 9 >[~'jK*<gF^^̎:K8vn4u8Uw :u jNl] Rp(XN[# ^  (SKW`QخAm1Vmn4 |rXI]-¯G}v{5@/~*&5 ᄌ?6|=K@x0Xiͅ/_}eD!Y#*ӡZ٠xh%W>F9(e(m;L|NvqklO-zpUf-MM 7@W0ۥ8rcw~)>l@\&JS@̺W_>"HZ8e>zcm%b%*$u#9ڝ4rd'|gۤP5|+}OZ5`jmx` 7\giT ʡhؙ9 EÊ/rXx>}a?9͎<ƿ s̱+0욁oDr2Y?$ɢ $ص bp)sӕlWzB{_%Y8vj^_*V4 2\+r8 agPh{<-c.!\t5]J[4kt;4L|GQjvՏEpI&r[63hRS/چꮭZSgkm>bcyêhu&cj 3 dNB{"II: _[n\ mcS΄eKV- 1ouS+i٫'nF7{X8bmNW}XzudbO2`Th"R^$VYBkO(K6K?'4uhQN"_uROh@3~7}RJF(-j wjZ͚c̙+kldlK4iFMr;K&OA`琿}w4t$j6llNcOނގ}d_N 63Hr2EOp}Vm"ib8 [uʚL]Q$3L#'aɭL\5tD %_VWgC$ }v>Ys5i)ϋkۧ%>aJ[kyݮa>4* UմUcmUc8Ԥ؜8~yכ@n Ul[-WU\ЬVxMGX:" ;` UT_%}TW(q 1?j 8:lYO,A"ں<ʳU*V +$I'<5:|%_/zǴ h~QgƩSרe8/`x<&¦2_|H}c IV)޿(?c=RT e7,Q[yΒɠ&jII'9+wǠ0P{':\4 QG }O.&.H:mtĖ<%)k[E̱wg9ѕ[!6M U>g+3::y 3`TV9mUwZeUDsFѽIarO;' jXۨ0;M2%[z(Vh|xm` W?o8Lĵ]M;eI&:5о}{t{2zi^??DhXTI~ADPXm4 ^lsZa6~Q?3fb ^$d7G;d*hrOʯ23+-{Rjų`_Ge֊@p.w; 8ؒ73qgƈ8ToE'cH30<69ժK\[(jQ~j[ZH自/[D?Io8R@yJI5j`3i`G'c ځM V_KRn 7Ŋc7vMnu4ĩ'~{~kR_ֻ0X;Ŷ |h 1| _1B4uy 9̻š*~ edXla JQ||ZHFǢZrɲŋ\iAe]{(&Q(9oxbr჋1]?X5R_j z~yvh'ѪЬp=PΖe n]^Z6 8lv)wdt rYۘ.=. ~A2a7 VgBD=IoNR$c*[OtܤGwoM0O.l|ĕa3͚[ZYG^ W5fBR@QjK#H5]hlטdlS5yGA+Yi?ÜL•:e/#-WS vG<|269N9'{#c%@9 Jw z-\#8V8{DFk,)0U͸'Y<"l:L{9a!Ow`˓\zWɈϟ'e +Sւޝ|iQҕc[ pmVT;qx6Ey8s8W{6@骱UY5p#׵0ettd:T4zU`EjVEC1F҃,H@1.$P!!Hۊ7 'M\9v_檼Twg>O&Cӊ/07OL)r*Zyg^x x䓧--"PdGЗb{۬pu8'8Ka]PEXp +Lۈ_:u>Fga4Sk Ⱦ^n[ho񢽆bl 9ě4nTM+xNG.3%.Ag",;>:+"hKZ`.%r*q^{YM:֦EP8M J~/:GOmr4# &;ŕ^T+Κ(Rr+FW&CY: 8y (t 7+I2ŕ&TNӊ!hlIZ)򫉘*-DA5oY9z1v>lePyF)Ԥ4 \~o\}۰^R'6a նBl]uWuZ+cj9ZqAZZ5>U* W$Kf]qe:X7#XzGmJ@@yFg^3|lr/ e_e/GAGЪ[!CTܶ9 ^3b|٥rl]nwGuEmɳ%'}t&> ît$,#`XZV<:PWZׄVƀ5%YK= "|iOZP2)?Y[ϷC{I6k !,z{m| ؠfiômG@6%9:"@WUrm 0[gn'd EcO6 ˩&pVAdQ4-8}l3 keGBd/?m/ -۶Ź瞏~$n{uӡK4sMI]y%$o5zM'_OGi?xՠL Tj﫩ܾ2,Ne3}F)6%@8G4|U]<<())%jakEr8`vP\7Aboᜥ83qz"ea85DIT룁o}.ڃ"7 j +| w W]ao WZrIqjK!\Ti;b VJZڸKU6'@+1\+$yɀyq 1K=h@)gюVYZl |ܤf X|9j_>[[[%dב5ic) XͥR  T~=4j p @bO93 _906_,4G-L}{[ .<Ͻ jbZ:-pQH@O]ʰhG Rt0,>ɧDAW- ~]qb%ab|+Vl&갲9U9 Gk> ;}/dl\$D\ɯL8R(bcU#T'; I0pɓJy c5ޡiiVoկ&a8n:`:O%T롁o=v莮;Y3NBuzl5rrY%y+ֶ;E+2ȜkX&V{?qG@ fŧhn[\A_&n ĥ0p-wZ?JD[@R.IH-oݰ-640emބtޓ-|8DHaIK#H5& )< ;/oXߑWVw`#ĥUڡXy}O$V=1թߞdKQp^ΫduwCp#Hw(~6NG73--qh?Eqܖ[*=[^LwV?N,C׆+N0cK߂J@ ԍ,8dVE{+"i&FfYݲEᐖrJ1)VaHx[ofq^b13 Z 1uAf(.5>wF\< nZ(:ZwUnmkdAzfF۬2n2t'n`:)A#;V%/+m]~7'1kg {#L.mfe+@fY~UEջhJhDȊů/vヒvr^#Fǜ} GrCg*WP6MLfVh*.EqoC#0ՍbO]QЬZ 0ϴQNAE$*.9FWpMі~%Hm~ѿ^ݧ%:oל$`y}X} 61<]q|,'t1d`^^$1c+ Ξ|@V*WO?*ƻގژ|RGq\ 0-SQmOgVe-%0RJ^Vh]- 2,_[3w/Dy?zX+ݢ0iyJ{g,_,)q~:Jnv;݃F튵N o*cl٪u^''-c&#N?\W|!c"D iӉ꫆$XPG 1Yx1xӭA[Ԁ*fWhF˪1^7s7g>^޽3P,n-[o<$N4*YrO me˹v_T6MKgQT? şmc^G*c/U; GXhX=d9X|sWi gx}˳Y U\%!D#,Z2s4Zӭ[7~o^ϧCzH{&ʲ,r@SJJԾḰ&+NdMsIJUʋLFn;aČtS^v9ҵ ?{&.0 /č}ቋp{W/C}L9t2Ufc`X4 kIdtep['^[|bnWZji9صe]Ң3_/ {7@Vu1P{u]P^Й+I M}WJ+gKJ|'9i vmE[ 6TR o o,Gjr|:{_25[4Ɗ&`^4 +z[j?ޫ&_s8FL;<œAJLѥEvA\1q$l/3ԟ{R@yJK5Հ&Y'k*Py[}wn}۲Z)6ubjɯ~ln'$bjd|Hȩu{KnR5#8s&(GMSo|&JCSkH[X pM6?dB>qkVQQNF"ljovWņw+ǫ ?V?ϏDb];NVdg5Op\Of8*ݔﺒjP]d-mM%3lSx`Pza)l;{_ϴ-g*I\5.=5[. @ԪW wm:M-%"vjHWuj^!󫎫Ufk4d %9>"Kh`F{~IIϛ1,P?\qۍU>0jhF7m &]|-}|u.4*mmN?nee VV?GϤx2jH4@rmggdm#n=&Gj,#6RƮ$IҦۢ# (-L1Hedk&],N %pOE^Sn^93731 'arkKVD=}l+&:[d]R徵Z+b3+]ska aC_?`ɺ,To,jwj Ok'n[%k ý,/c}e9U7䈺´BA]2~:}%9l&*3ݵkTÞWS`qqDjsswR;#^}Uvġ}z!+M(*?q NM뾵Xøl{37Ce %tf9w/e ^R *:u.WU"xw MGk /c#oh~h)iXV|˰2,Pbt4!yԘ5 o eN8cZΖh%Jbk=ؓd.~UDY0T-A򥋱fc6m +[^:RG\ lfp p_Њ7CrYdVh~M/rKGʋs7{,=•ݻLgndf.-\5>a ;JG9{iyr  * @GnGq=dz!"ƺAhA:녥*F[V@βDƸؕhUv|x wH|jO+}z`4sJ^bvۿ#ՄR*6!B<<rv-_Ee8KW^m7ގ!s,I5ieNb-ҸM*cXO]/{u'7H0j`@2@XnG ,P,0\mb4y/hڶ#v `Wqʀ';&Ow RZ껌7Ȃ;$тD,)b swec1'yƠKFc9l޼r4d:.xo֤h?lK@XU vp ~%R' G𹗓|ci@N;KrCeZ\krE r̀ssGdi>F5mڴwbvkV⾏j^ߌ?\V{l=äw؎~r㐛6^Ñ|26hnbjDǚ;ʵq bɭIq80˗Ƅ'ⱇǀBکj K6Vg;vԱgWlwVTsư4>>kHe=4j|oht e&Vζ_z +DybбV^vԉL ͚Z"Ϣ|9MPDf@%eB$ x@,G'~{g0{26p)3rSv3v }kY:o!i[r~[w59c}gp kCqB.YߝI~f,-t&EujPԴ53}zmAW̊wEPC- 06 j?$$QL#W;/Ѫ7hQP pv}^~CxC/GܱܚҶM xJn[FOW΄AA ى&Xd: /wl(?s8th s5F%ˡ3$$T}$krV.x1N?_ ?KˑWO&ιj [_;뫩/DDm 'mV^;oyseԬÃ^Kc+mUY9 V%Mi\2N[Edpxߡof<K%(;ݖf P=J,r*<ko]Æ 1bqMC ' jeNSUIqE5_}V}49簭mΜQxxp8pCަ+\5A\+8wq+WVvZ).V@Jp,^ rEB+4VUtbW[ZOGt@EjټeE8o>"8֨RI͏Xztu7`}DZV{6ǀvA&:>Tu⤭BM.`buQV0"0@nP-A#+e|{aX=BwnM};&1>TyTx䉶VENR<mkz7[+ꀰh"-{y2|q졨F{޻íL ӀI ƬC.D~D,[9 ֤πb388r,XR5㿑m%YZAfXm3@|PT#,{ tE&rs]^G ~{݃ hy,TjN=eBm~۾vS;}4P|J;Bq]ci[ԫW/֮vjp/f2 f5IIJu:).b3t s.ŒκJO]PRw3tҭخQ:7,@ sl[&!0ֶZ>{Zjmæ&L^;݉ڛcYQTwilcuQ][7]j(6ixװ`m]䔴i@%2kNwǝ`K_$oպBů$bX5"j۹\[4_Tt}_{X)P,;շ6c4T~0`f n L|w ƿzm;1]A |"z/A`ɨ߷5J&-gMVd-h;z_}j:lŗ~_%_|E?FN͚xQإbbee3B͏?Ѩk39gy> IDAT] 98enG`-l-ʴwG&$B@~#Hsmre--#}@0.ntHNe\bxSdMΎeӳ|V|IZ5-5 "dG`"K2rKcyTCpǃbvjT/HR"+ʴxΫ$E!ʒ socoLĵ],H͏Z&M/>~!v:; jjgۧMw\S'O"M&9D+jpW r<S\wtQ裏A(**BݺuQjsQwj[|JR-[ K.Eqq1.\ٳg;,W6thN)?Wãp4o5GOM]w S{_|~قi) j^S 4AqX4pi ?0A.hZ|kpUx-0mP!w[sl31MCa\1Ly1зz$_c,J`+ =ބf?IQ%ʥj*&S[ϟz)WcY8q!]8|'V`:8iM&L ܓuBf`z|<۽cѷo? [Vjl9^$s}ybt9(Ұ]ci)4Ƌk.Bcgxbs- B Hx)h2Jm46mJ|Qӻ~ұ.jwBj~|3f *+uL_W,1N Jg$D:Vzm;nj,] <7o5]og dR@nkAll-Z `ƌ||&t\$/M*|BK2%4~WOccfE>N=L~ =CKjժ\8Ѯ yQgU{dSwͩ8IȖ:Zvە0l4U7ߨ Z+ 4~|ΝQV6A˘}w;E(f[i]g}7T;5;//2}w@'2ٿ;drV6bS2<8_N/.ųO}GOW\qz&q[4]z%c||~wqVqX TG0]¤LMw^ 2,yxzcŐ-,&6t"yC.{\5Q\ٻ맫RߏL<^-{Q4k;?IV2{đ#aZBl$|LŧO/O^{Vg7xݷ=a-'J<ObIsuKO9R lХUo쳏UauqF)nV@;g%.wET6=R;h@\$:#=%;۽1i/ĶێZ~@pk{=ŏ?d@&&C3&Zռ߶1k 1\VkrUx-RBԻ$j˿%bYd(ˁ򪼬UdO5 g%S5/ D]`UEB%zA@@tTQ߲]{YW{CQt4N%$|gy\_BIvsgwG1f=oʋuǞ'^u< g 1qRi9:v["33S-|) R 1+MWfˣɣeW\xy&{ Dk,G7MXEqɨ)kM\L 20޿l6 dli}0NdĦA5nwUIG3I c̘1x慧Qy-4Q{E~ QAfBS޹ZIUUK]#c2y2e*oW#sbtاr;88D_ٳ85Ħ%vq5/N& 붍?ȃmaI ,|jiryoZZ^a.mm)ʿ[UvkfsKa$PZ ڌ7ܖ~k;o[k~E\y, Vj/+PJp )ˋ0,+yhe`Eѳ-rɐ/B0'Zf!j^A6-k|]{ٕ#Cgr'zw@rƨ(Z~hy- 37w. /~Eꫯ嗟/>Ŝ?nhڬ&4MC*W@l"I(6mp+spd. joA]п:- Ne?>}]|X`@Zn=z虢ts _VlO8̶l+m<em#1~u*G)-DZHk,ϚJV8݁@@ҳ]835&.<9?F z令! ibZWex#Vȳھ:&U.O&N؏1ɨ\2j5j?Tm*_PQ 7TL2Uȶ͹ȕ7򂬅uIڶ@UѫW/m2v"@s㘀K8mYD-1A15´ gtvrG&JZb[8!(&h*x (~T0mi.b(Hzin"]VHG&@i%ֈaa[:ڕ0Xٜ˰ꩇ7PSw4ܷy@@~_dpoߏ-]5ŌW4o7Z| !mP3P>s ?y+?m;Ǖrb1Hm % )y軋+AQtE/\p…e`% }ŊX!P[J5TZ͛@Vtc ʋ,Z=8{ر:͍_RO?{キ\=uEa􌧉'_Kk4>7zL~D覩+Mmlt^(#XXHq<hUEqZF73Cä+k{~W.BͪI{p||_RJlu*jfiY1J p,?̝g#F8@0-U2l޴ -Z@f#؟R8؆#@\2A.rlA /10]C@L_%wDUX2@L؀1ma;URi݈&S%e:xcƌ2eJ E$vMlԂir.g)lXl`v-=m^>px6^QWpLJZRNm̶*bm^;~5Ordo\?m9cL:b)Za/# 5&c¦ dLzji 3=j3Oz0w&R݊Ĵ*J_\{X#s4>kpntӽ/ ?w`{18cѭk_s{' My7J%ɓcpM7Gp!6$[C jr˃6/ދ9ML Sq,~1R@t lFΦ-cf|bȓ#8'.V W[V>yLW[ԸZlħ@OL@K5ǒJrŵͲ駙y.w2Z\o$&)g9cP#Q N1 MNga[8n6vaZoCZ%)'շ~zH?Tz8tIIxX~,;W"~s7y f^r15. / NK:S0fyHC〱skwpQ֠C#TK<,|t\a'7,qF=q9)$B9঩BD$Pؾ"@\|׼lbj16i/] rn<9>!eӧͶpڦQ([L +>o/w?_;*6 A2ȷDgiHKզ?Jt뺯~ٯGwxTSzb$<@g_ړakWbabܦc@^.d|PZ\Wd,kn>Dc{d[Es\k@T6%M.llnjvO)k.6O6-4bbG: r^uQh;HQK,^&3>K> ʧiOq~,]8[8|~h0KNa|`㏺7*5NUj֬nN<%xmeA7__Q0A44a?oř0fgi^tr%Ɇe'OrL~fZԑܝwVVwcY)~lVCM\Uq xEYIx~qY2}<2{!5YkȎS5tA `٥g֘[!:# #mQ5i+7/㜏:{WG=YgH" . XL@~` tplbnKّJ"1LeuxG^4`Қ2kYma_8ڸa=~ @0а|:xlzXh #M<wRWPWN_nߘ4u|Xa10SF6^y/@xXy ,;qQY? ߙ`BϞ=u1,6oR^Fkns=[o nqd^єޕ9y;OYoj̓mDcVL9c5 Jݬ٦v0Vɕ^f,3~ ͼ *oDnjv:ksX;n\&rF,_;[1S1ʋ- dYf;(E8Ehb.$I F!.yu`ˋr^a@-4QFϽhs,b m4f:Qys79si8c9VA74 >M~\>_)w&̡0j;$d_hմ4~{AK÷ 3̾majr:rPf$t0'?>c"5{y#?Oi-(ײ0%~}s՗XVX^^nls:0f$*xF[׬59kVQ~2uPWIQQE(iLci95^q=/ s>;̛7ookͰx˱l2r/ҥK571Kmjˢĺ6zh<s_l#Lq]\\<^ a%-Q|lŃ5~~jkYkܴ wf#rZZ櫠4x20H|2xI8wPO6Y13~AZaxWJ͠fw֢ӹ|$mf+WN=yyV}~8gtJ.䯠tI%?'t؄~^0"I`W%6.9>q<4eɻLo &@~곏4+܀"@å5:s[QmafN_1HκR$߅ؾ,mƿۑl/kU˖tg+ᨘ+6sz4nupT\7H !猪Gbq^O1'aLwSn[S[Xl{>l s1 63ʳ=/6W KcG)*Ͷ;o3 ɏۙϘ_)|!Z>*ezH(+Aۡ<}/V.Y }ـ81!_6JǐvݑTLJ)o ]kqvsRF(,}KۍgX\ᔥsiwF1Kw/d+ I:䷋'!:̓nj fjiPKd_1KlO8 ҆tꁪ-tiPq3zV?<7Au]{*.^ޢ8kLM.'/Xg(X9"FWz{.x1w+(↓M5~a:+ZUt1wN`ZcrvuǪ$Y;xn??Z/z=_gkn8mD{aqGG\"yoY- ~esOpcp+ѡQ-2_A}:.she: =$g H9+ߒ[.; y3DfNL_'ձN@q'Yk~D4z?R4N08p4>O0=A|z/w'n~_gVR? ?^>czqg/\A87 33w\/:܀35kLoe[x@v5? Fekɲ]*~w a>Z;7qyqM12 7sD(-psC5WXuS4qD&El <>" ~ yʴ h20 >ȼ-xM>oa ѓW\zeYQVcծKkż ӟ+` SQQ6b[QfX d˵ MkaDzu?cfeT鑌;oƉUv|:yET^mG`!O͝]u؞ ?ƪYSq≯R=蠛m6d,.\Ә6 mt֧'2:03>~<+?P_6K}4M6+''}t m,E]c9< O;tm$x~1d~xWbhL;KJO:>}Y?oݎ)8d6*֘5گ?nɝR.[.-li)и{MlrbOյe-Vzz`Ui/`gp%GBtc2M9-0y+.wj_d@Le)sЯQU]O_.՝jW6Ȏ$I @$Hj jX/9W@u2V%K/(}$QDP:kn.aXuh}<Jl)ݛŷ^)Pu`> <āam]r#QQ{u&)k7 3PV@V #N1ik"ݾ!Gժ)k 3~ȞX5ֽ xv)ۚ+BP cL|1~~e0ٱU.p(xfqax6w, 6FO㻍mLx0iy,^X4sop86lԩgϚ5K(,$_s5x'u '@qƏo^hsmO?ӵ h6V yXƅEivZD\ҥ&5̜tz,ɖ6'Kpj*cMaݽէⶁ1ѭw[X8f2kƒ+w7?>d#ۦeX:s",\*@A$eL.}-ZTOƉ{eaŖF&:>7O|EH" D$g@4ap m5 hRe:S:BJna ~,,Jr'U#T/XXPu?pԿvm^fZ` L~djg(=Kf;7L욝vH̎sw8Oio"eKTN[F۟߈fHzdMg!mؚ:!믄N lv1:޾m+vT%m/ڤy4kxb0i,l㍇gv,$I @$H0 YClɗ~&px1/Wq!>(}$և P[#-&a85YߎGI yJt<<4o3VR'<$!iK9Oĺ/ R/4K_,ABX]xHyH4BQyqWDʡ'=cX=Jj K rq6faX93tŒͰsw@>j~JHHCڢLq<,l[938 h.OQ4~%2enecZƯ8ȗ;IsW%Kf^<#ތKrH[?H3fY$ 7:@ \#G%\;dsm2?. /*) - /SvqEZX4=XfjTdzElS.qf*:--Cղ:w .`9: ڸ=0e*}/zWɌc/7vi_>dY#o~@? f*0`nL9~ rղ#VMGYNOnxFo+L$H" D#K`h)buϷD_c6a84Q_ˊr1ymMOPP7ջ4t@Rܦ<'.DQ PMPgڌˣʟqPWT66$uwnl-kdl/RFmN¨;ỶH7YW.yW?ZMX;kLy!EXixߜ3c hgt/ mI4>o0ݮ yQamժUKGi5'x0]9T8̋Sos=zA;LgddZLpeg:c ^zF2}x421.^񿥛eݿq^+6qn(/֢5n]*t ,مKmI嵏(X P%v;FO.ZToG6hxꀋq{wuƒXbe.[fA4_,)"0 |oRVodg{?_3V}j6ܥ~# D$I SeeeӊQc=8♚C<FF~YT(bKlMy"ᐣ*IOÇ</ig J Id$U$`)wM?z]w(@ޓy#￯G( ׫Wdh^Z54va]]N>{ qnڴ) c,Ds28{ßqPM ڕ|ݔކM-rn@1\ln>8fH[|(~ezqΨ-y춟$yqkG̋t|}W:0ҭۚ6bmисso>\L>#!^+d8Zd׷ Xнi#O/6p_+o3?ZRiwu[K.ϲ;6Ns-nEݣG=։ZnyL}NGB0lLI[Mb#'ij*oY kmct5#qST8wԐs%YC׿I.>%xae'''O?w32 w{u7|jM-&O3ۙrҥ^7Ѝnv=)y0/̝ ?xPF  Nux( ˆaǒhй6g|+6RSL´,٢lڦ-k/sWsY s7eΗrW`G)65a!}P3ljqm(ڵkqI'@\r)iGUP62y?l R4;Jj*p 5љbOo~x1b͛@m,;wV`Ov,ɘu&{4;L6駟TjjYg4_wuzv2[?p6ٝz 9Zd6K"F~FX"/cʞ P hɶ`.sIڷ+Bvi8Ilg=]|B[su.W:%aEv<ܑJ*{=6]˴xhm0Ec*/NaYDɏU3Mdž=s7b 49>RPs/&4?,`8ߝ-Zxq qKQ7ipI袥<_AgxVYS1L gc3v]`%(߱* `.aed cvY;܌O⌞|qFcOkae DJyS.=o].,Z\_Q IDATIlϲ;^W/ .ԝ Iøx&\he4h_f\zH>wQڠțFK4t*Өs1n7e.zOVMSc)wI{p6sȺK],w+ΤǡϿsKVV㚨ݢR#f٩!35y+fYγI۱zZ\ ihŀ~ѧO[ 1KK}]GMt ݑ$+p)}{cS J^L1ߢl$|7+#y_w. Vܛ5N:WaE WXfM |Y_hzH%R%VMiuYge2;/VO%@: @ѽ(f8teRz,@ W'H/N47 vP0`f'b@ah(A1?++z|x0}Z]T~>Mqp>=8U;vn|uw#lC*>ߒA0/{_~u,f;銻OИ$ei8;c0`O>K.M ̩M4<8ݒ~>kh|[د7٤f4sM+B|l[-dsLY6ָjH"yapOl3edrYx3qS: pnڵ<`щ،\>.?c9$k @(ZX7~cnjEl޺oٸ,v=㫯'}ߌo4G Ȱ ;8ep4v1 j[6,~%f9F>sza.~eM.sf@0]/{=]Ù$'%"<裏tqbc00.KY ~e!畇B벽`[ 3Jɯl'~x sU45-E3N.Z uaI7&`,(KXVB].'5 7[bܙȚ7;r^@`߶΂a90lImED1mqxG_T)MEڕpj_z%{g}adY9Y~G^rM}íRW8i(##C0%1;=/K8oMp].Hs9k:"w.*Y?S9큩kœUZU֘-oM"}.IBㄆV|k,LH0ü/̓*VzƱlNu+m!Z>K6+|i7{GYǓHݜKB Sض,= e)+![!$pwEdF;WZ֝uklEQ[4DلJMa((M{VϺ/@OlEiv8Ģ@wfpk@H8$5hThmia_x Jv VRGi ӕ6  u}EvҶp+`q\ ڟbjX5ǧcګspqȃjq#޳wuFΓ֛U.ig%wzJc9Joua,so~+w}wlf29V*grG͆ؓbwxQJA 2(7@>.֟PeO nHM Xb`F2r.V`q6+2,Nmt4&N]fZ= {+SH@FȖ{H8($ɦYl} L?!njf h%,}8O8M8kv8>_?Y'G^I&NԜqm5kvm۶h׮6mKi=;\rX6i87vY:tͶ5&%%@Ɨ@m}y1|Ŋs-yXdN^^zߗkmqMt3Oڧ~Z#gpGmNnРʓS}@L޼-[LyԘS_|q!ZG_TN*!{5ck E r|Se4+|}9˗uĊS僜?͙2g>M6 %+:ͬ}Y; xQJ59x#p1NfHT.zByդWf↞gמ2\pC<u$j7c~Fpw/ZS4V}>?G&;pXxѨ:?^/~xZi /4F9`D^<[~a = X9ֽh[8RR \' ؁ v΋@+: ]>_ϖ_zh,k1娍b:V3UU *`@9ktE{KYV,vL2aݰ^NʑwH K Ig7l[akl[\|\ߏ3>m񖗕)l۴pj) Ҹvڴi~ʋX!_@;e=΋;2|ʕh߾NOg;WC+rc52TΝ'jO4.MKm8ezΜ9z_4|xorãXWNoXXh|LC78oSO=UV"M6mjz񡟲ѣoY?va|.h*vZĠ@=VJ#@h<$H*X>25Rk{ ?# ϼ]j|ז&/Ü)w?־H*~$d:yN"'%V/)VKG7M!_A]:RtIKM݂ge3NN;?| d{f#A)[s4[CQۧjF>_)Ol\$b%2o1qP 0yI}8z 9 ihv.; q^8fZÕi#o(?#S*hVO[V.EAGǣBZ-*Ԧ~uQB@^YLSv2?3%!{.i--0ͩ.qӌ3prC+jjmD6lP/[3G4|E-2aVg|=s5vgƞES,+{)|x7mꪫҍ[~m՘1~֧'-߂EkL Im17jL<J@q:ݚ:9d,Ǟ H<pj%x92-ȷ6Z H4!@ZD\r|<GΔŴZk|̶y^'T ڵ@({!H&PxO qN N.$%9帝3 VS6m猙/kWsyє=E|p\wKӾFމ Y;A_bg:ڏG+eYp!z>Lkq @ܽȨO@,*:m5:@p ҫTx ՕѰk?Ti,ؠBu68qk]=xku|\lH._</R| DboM ~D^z('`L%3$E&iVO/]db^W9HHQY|< tt7;/BkW{oXRK )b8S .Zgnbӭ[7:#8e4l7\3Zljw&NII:C{aF:`bsbuUT좌O臛}GL-7JYԪUK4~^Æ  cKo|"@Y9lP>gLjȮ2X6e "Jf ^~4VGHZB(|X Ҹ6@^̗i ev &/¬՛qT*xoF׹!qKZ~IC}mbQѵnUx8Тg0M[YGû4# v}xɆmZ_WbVY.Y5ŋϾzQ?/e?A)fd2YZ-۷-W G/f? UtQxq\C@̱3ǥYko\W * 8_VM*~njz !Jɯ)'`oJR$h $H~tdSuڧb;ld\hf8v9AYoYD9Xlmr|aQ5 _ =ooq&wE+*p:uwU0HYr̩( 3F&?4PciSͫ8CM-E]ug:T8A00.Lg~ߎ}?sq85G( d~I~W_?_Lc񴋫OaepzMXY6ZBUk("Kh2ks_'f$ӦSU񳏴~KשВƺ]ȏy%_%of; gqq3]2?\ʮIKCVV=e#kp'"-=M"0$4 Sxzp 06 m1ǭ*@*0 5n:6eTHINx$qڛG{&KtB*B f]Us먉!Z bV[ gd]ӸGDsu89:lc@Z{b{ 8aF:Ц'A\>_PO=K! ݠ&nKcTr2.HJ FOMP@Om;7K0y]fQ9hz a3_c0x@4ySevQ2>=3Agqt/ZH29R;J=#|̈/s8<'->o̙3S2(R}Yg){:3T̺@/z?/db~|!Pg}n~Z0^fr!ZdOWrݣ~2xߺ#gӍ-و֢5n,}R9Vu`}JW桨NyŽ]ӸjaAJŔbMN ݫ32R0PzOM!kh.LJv[gbRa8huIƉ]̗5?_FA1S3Nb {I~~w F pyY&"hk^.CXbͲqc{~}{Qcs߿;1/5ummq:Wb䷫Gj1gzz\@YןXmc$llL6qGS0`/#K ΈfX,)|ұ8B%Zc0lii43 r sٔulti(_Tczi0fca:P6c6Nc'!t͚5 >9NM&y g<-[/Ԍ4H/r7Oq/A… &jرE㔊+qSO=x@.cN)zGu 17og|yn|J6ǍǸa G}ω=C;3<o~8spw_WMڰ 3 Kkn/RSq7>?#Z5tX(e*m@Mzuݲ[b>K]זJ+4OM͑R+4zEpxʧ$hIe%2)ѹn*Ɏe1]wA5G뢠\[@`ˋw@`7DfXD!]G#dj(2X8NyUJØ?۴%P>#4K;&Q15 >3| 0:2 0-q > +'a'FW#I8s3x޲*3rMZBb YkP(>gNu \|YH|#ߊ/; D"ߚ)_8uL?l,-n|iʭw"#f.X'-4!(ņ8h` Wk_qRF[eJtSk [Db]V LAc(~9U~wij[oFnrC375<ʃ`i|ƏafR|ǫ ȇI}ך-yxr:݈k42%m\)YKAtAl;* @-{AנcbqY"rCCWvӠ adlg}LpkPqUvoy%^NK9liie' 8F!+^%, wȫfGۅa3/8^*E[7GM^Y F bqo׋oڤ<,1Gv|W‹ r,g?oKT +*KIL*'7e q:k OQz0vvlަ&@WP"77Nt:}ZDSp ˂4}ɒM=ϸ=r &@a a +dTҔ8Eo i%`iG1O0:ƎnQ_x=y̘1 %s.WcǎU@|g*px \pN&8ٻ6@qtGbc9y11i`\sjb??Zg~=cmt;=G_K{EZz}(&\^d:uʨ>2s*x +ri5dg=ҮSTd&@VtL!4bf9buɀua 4:>Od[fMI}kheJpGc9ۺbf=5 ,% D2р } ᫡F/EwBHTㄐ] G/m;R7|-11j k+ٌRjOC: 3hGU>*YV ZFT^6]C,`u\ͺ&|A2#1L >{M7E&<6%5ȏŽS8e๫"Mʿ5sNz)A]dkobיxll&N=b17'U6”d`q#pO_™8e$!3&M uoĀKI%!ۺ O?5C/ ?'˚Wpd8ܢO7J-"{ȍ;\&X]0} 4sp/KǼ$=#yXt˵᧙g I3P~&0/Tq@:Xt%]eF%ƩR>Z<4ujU[Qk,+[>`uCU׵*| I*b@\jp%ZvZP}:Em̓rnJ6ѮIkJ 9#㐺xU- ^-{al1:ʈ,)'y;4LP9z5G;3؅ɯ(8LS|ă|`Lje9d~])[1j !P~rcO/§cB 2ܷSG~sQ{~._y8Vo.@̬p`[o"J<OӲNWm=q'G<NR [¨r|n !1DIa1РB[^bXy\~|pո.(ꀟ#O%  ~JL18 iP^r>p(>xnl <\T2ڲϿLo:|? [tY=m"}>-$5LQC6;s'ׯ9@&I &u6?6og<*>~D@@EEQ4J,clP[-jb,1jbhL5jX@"R.ߙ9wg/.e f9gfΝ{cF4xp }m04@^NVP j[dU|o믿V?ʚ򨭮cdf$iLrmݻѣ-qvdԓ,ni6rVЪK>-O;hLfC0{b ǏI|q"~_z%;֊%Wh׬߫c[yrlAG`tMG hR ZD5T~yzzY[a k%𭜕Mtj.kڮ4O $àad%3ДiK])2r|ٖ$|o=^4Zͭ |yκ\uX+&W` GG@[ESn\vRyꚋJA}'=6}:3.(Q|D^^.(oRoH X;-A9{kcDlMgä ń/e7Dr㟏Wy -k?{GG})gm ПH۫%[{3e@̦ZVxX ƈ7L{E3𫩼ցsln#K +@mbSX~wwMX"_t9oL><3~jkkzfMV6w2r)bnδYr!Y"}w z ~4C#\Lγ XSͮ%W٥};\n96j`-Mmqx Q 0#4[UVfiaZ4j|ɋwM꣭/@2< +ᅻ֖FL;'|`k\+| p D3`7`soUsA4 [\bz/\5vr4!oߦk_ʱ'+5_^plNНz^?+W/_y:87!DśDx2i.7J^/ kϽ y F뷒ZY~xۢelhU/l=Fִ]6VM/s 7>Qz-ɃhrώsXKgV u.v0Ct' ^PڃjeeFڿ׿v>kҹ:ӹcX9*#?CE#YҫnM `|:% JF,;=O ;2s1v)G^G>螞q-G8#M@'|b`l:|XWۜ9|gZ,Tbeq].|s_|rUW<`hkCgKev~r饗ʝWrtKvi/n͏}23jMg;Hm:Jp^Y5Ӱ/E*>BhAuo?6J۷FL_[$uBwޑ/<_~σilK^Z>wFK8kwApAt0ҳx,U/V9oDʬ)цLwݫ k&D9V(kTkF5 ߸Uh;> z4$ܷo"<]} VٗB)z0]T_,?ҳPy6Xϳ?mz6~j][>v΀]"?v[4]~3ΒΆ5wj+u)ʿPgbW7knw`k";fȼtkxEarxl<R1e2+d-~';p1dK9Ҥ<%[S-^1?mE4[87Bt[´ 7Xoݹ g^є |d}~>ީgKv3}bht.y }O5 h$t֚2*.'pi4ϴ<䑯c>^Γu>h4)QFh3:ʆ\y(T64@ج.?וy4-mS.3R_;uvfJAqn#;}6:t\/&YK1R~/SrUs}yw޸e6jnը~Z$S Skqk3 >"laD3 J;"O_klXʹ|[zn"wQ02>Үll4|C H upl $)EA|:_% ]3oZQkfdеi~ðp•@X׀P 2}c]WqUfyݾ9[l= =n(n.~=4JZ1nս<ς<2B."9ҟh+"L*)4*E MvSBJKëZ ]b{ȑȶ? 6SBF<$٭$M;gr:ypFNݥ(MuҚO>:T&u/]&>~Uzr\zrzܮ94 nUbx#8@:~=Ci,[!`C(&0|3gi+ӻt٭[Z{ Fw )w?3:q?(}UWnzzyz^錌z:lN]!<-),+Z4=t>xYe*+2~x;3`T#O7yZORڲ]mO8!ݨqLMX<<2ԉ8 #m0wܲ\٧\k;ydSO̲ Qu5 H;,a ƖUAhUxl]y[[T@C_Yd9;5ωŕz0k@Xqp vms/aqd8; F#VG>B~0XU\RxA,&ц5;gc2@ JctFHۍH# =V^yUܷ UAXҀAc6"0to@ktmK/+ϷO?<\AVWmI2T_*uQM` 2) t}caOH=PVc}m4M[[w QRneR9fDr=pRWGy-,=SpϹsy)o^z\ӧʴ#eW SL;ah~4 a鎗ThG\zk*[UұUrj{J1b)Sȣ>j`pmͥ4iӧKnL~^ʈc/y{3ʍc#lD5i3δpmv~|QGF9|4>|Pn~{mNM\[5VV2K{^b +ib-VҴW39K ?U6jc 4. au[3]dzdUO?n^cÈ>x:$ +9YciIzu?rdU|E/8tbq+Z<>WPOafiF)#fa-(խ4f?CZF?q{;,_ |-5O?V;mƕk@4kX8cӤ^K/K3/8q.>~URzBmKi:MBAy_P-Ѐ# ;(`yC}l$_[N<69dܤy$1.Ǩ]*t:W Dڵj?I܅N@YG% .f.KG]?w(A̿1n,yrG2Mu[]njw}x|o7ۼk c6sgPgF}pv[fݒY!:"4ӑ*p{A0qck*nU>^)= \Pe7hSkAC ɹ|w}.SHi;yuџ)ӞU]Y0&pLu8|]lMsa@o-cƌq{ȑү_l>jʦvm6L]Kn@:};U׷9O먱.7bq&:X9ݩ+i^]+qV3I F=񻚥i4y]KSO;Iv:qiAtaPiURWz#AQ-:b^yX ~9s(yr:5} *,"^TdHkZ.˼L&LbWCݾtXܒ&[6hmc[ /Άz}ZM9'Zr-7y1yz ׯJx9OA&e,)1=6e91&̫|=izP&vR;2[Ioz#K(( f?15+*HLd)t՝5г7Ec>zoRZ\.- t|z@4h:51IuYл\|i^4´K1%3L6vjӌy9n]זʤI.ӊlڜ]ܡC3Ս?S6m5Ād^0kv%YΨ:tu3f4i׬mu0jlUɡii6cYհ##g<]_ǒa[i.7}:C>@6[mޒo>L= +53*$ ~mJUȨ65w6A_H2 ;<.CQ>s~R4ѽ D¶FX7"llPdN'Q$L-+B9Jk,>)7  M;p+»Df>q4iR2̿2RJ~ׯ:bYzbF-fOf`+7DYAE"+o|~K[m`NɜݤL}@Ѵ3gx5mTy,ܠ6M0 gtjF6a@p‘@ >@S^g؇8AmSa@'|bke/L |LNJ"KA`!" iT239sBiYY2:\'>uȧkOpM!2ɭK4 PmX4 t1),rmڵG~)Ǵ*o-9M)Q^FFuLUv4N*"ب8eҁy:pjeͥNgĪ;LΨ;`0tlCJiӵlԫONp зʵ*4= ij5sy| u8 }ӰKDKp]ʻki(jU9K"qتЪ25T٪dG;3]R޽|=Y@wJ"wc<w렗Ltyǜ0 ?2q3ZAxРAꫯC裏dذai^{%F]۱cʳ>+gxU|ig%A˧;i:a'yIs=Ns]<4c}vO;4yMu+$OuyA//F{3f= ~q/IQ^ȏiGyv`d%ʹ@6Mu3zaԸl&^g;w:dc4ͧ 2:"ݱE#9vؿd{Z{6L]A jn€s` qhG9zl:bVkSj u RꨣVB YeCy)"JQ^tq  :ɘڂ4KT!)='BN9CJ{j|D74P]߈a&m& _Z=[5.VP|ґKќ'͟|J Jm]PeI@Ln ݀.z>g4QTC~Y;6_i?tӽG$n-I;;&oK@#pXPӱcG:!Ĩ&/F<묳lT^u;4\o̥)_J'xRB<)8]5A˸ʰ ьRYA.4=_vpLa) ir3@~T7eԩ{Xa|^\~喆Zպ9QrSelu6գvG`lci[l}&QІ+ɻSs.)îU5 eT7ϲݠ](ǁrFS!\0ǗH;#6LhiΕzzh)++\Ly=\L4|qg( 5ůcfܹsm6?ɀ>c'lf,ܚem;ݣf r]Z5d24ЖArˀb\X,2cA.7P0ܧS3^dwm;'u/`kZ㏰z7LVئTza6BȰZ1rٸ e1{GkeJWO6]96M#g~ymwn^nHm+cKt?Kuܚ?-K),חʸ~+zk-.`b~yW ?ְc %`BXs@ j>|G.?|p[[Ou^C9$u4o+>ì3>Cm'̲;'s962iv N;Zg!ӓ5jȳM6fH/$ #6r5q}|׻K2:rbΛOOuu%fqޗ{#i;|xAtf Pti1놙VK_ Y`xsݡ'| z;LVs},\Xޟ:O>mq}9k X35^ nW}ڟM60!2}`N)dIH1" M;hO>:IHϝ$(Ȑ?aTX|[y &jnu GAjyy0ɁeWQ6aV#6C/}5z;t3Pr`˯ q*Y ƭ F_(jhjn?j.:׌ctIwKԷΖrPrK[QΉ=m4*YF¹Mб#53:F]p:=~><b_h?iyt⁖O!])hLUb,e/v$.Or_2u{^3J>glp }NbS}zd^_)?62 _ɭA7ڤh>.B|0d`.W(+͓nB۷ԼhGX(0Q UM/z%-d J|C 6y0i"-K,kz| JF Xwƅa8#x&-I ŊlD2lf ÿɠ ;gfs iZ 0}P e)6*|0W' FRթ识4sUPlx[gq/tT+1bsK9ܾ}{ȱ˙@ rʗ.e!^]Ze5ש/5Iu Ӽ=Ѕpzϡ%wȿvܛ(!lJ=&^(Lf$l \jnһ"c6Oo-Ot:hSU[G,ZTg; #p 6՗v 5L GC[7F>R4s6>Ï|(+q% f!DޠHو1D@~i7˟͹20*H:_~(*W[ gVO});V Ƴ;`ogZ< E?4BC^]FB^ԇҬ_?{qU1 4%WpۅHZ^zV|5ZĺS+n!LuZ84t-_%`n%vej~sP>;$KFr`1R7vopӧRPd=?//iЊjF٘G}mzSB|=ySӼ.~a9f'NM<-{9=B#;h<`tkqwte-vڣ)0Y,qQk6c#L<+kn 6Y^cP }A2}}:+xvrhua}Rkv<;yvAT9IZ?A~/hpb.MW0geS˨Ov6:TUUj7z)(h`S-㷲4_yʯFdoP#,T5Ɂ/_yL )k% VJjȬ|*70p`JmV9}DalFU%A̼g#Hvڨ2JR_WtU+C3 Yߴ Ijzuz&1^Gx S/Oەݰ-B`|ŰpDW0bM6}qˆ(&ؐIl0Fgia/ ^^`pc /Ƭ}e4Rh^nCK{}O< (e#*l:~/xt-~l =t:vc:)Sj@0#eee65}]w6c:0h3vhc\^pF5ue'P \H3SڤuֿezT2nou7..SǏʺs5 Qz=IGZxVbL s`kk: @1С0OY(|Jg.F}0r~NFCS͂aΞ ZVXB)Tl ~zQZ09czUEBW)W_k~=T+s_|AfP\q̯ЮyPaK현m嗮 Q2WHU"lC ߩ !=ƅ$J[c S``bKn-*Sv2ݹNW(##aδilq}5} >%tOeiW@1~/<-o^E62ˆ~S('i7@i8/YO:x4ht0ȦG?V[ycgr3mW^VIuZ[OL-Ic](\}3ʸrm䤭ו͖;4N=ַGT0̔j g| neˠ `Hz6&e@ aց1>)׸P=qݯ3hl.s1_<R@R2 4 I|gGӂHـz[79Ba-=O0ueDx]FBiB"R-`y+-\Xj,Ov}+Fk{ao{7u(vLqիZ,<p*ӸES*|W<<W\YkjAQ`;Δ07ޒ|~h 8YS+}cT@Čी >X;|'(,Cc9ˤqh8hy!j⫔zpvUfNiq|N$ʧJ)_>\_Mi\[@1 5K8/Iҥk8V\ve6@iրg>"3=&_L>c{]Z57fZXshpu˵}ח6Y[9KY1cH 0'ԶL[ v+'a8cL E7ꌿK20:&aUd,ɦHt}0e@WdVrS=Ni31@ zUti%JA$ul΄=%L]yT23O;4 (Ilj*o?.V]Q]S45*<]OU#/HۿRP|b}5Viآ[lN~ִeLWe>|HXv6DOe+@FF(T4pjOy~V/ٯPR:ZL/g$։;P7x6LbZ,#:T9!}9J#2(r|YbxȐ!SΗ" ,VH6WiosμBGӝLE `}{wyǮ9hܸqkgTӞvۂ\:83-0|;cL].?Iz]z&ߗuԾ&v~Kie %  b!q'sd.=iԍdz/٨F#pf.{n>Thv YptұܩPT&w6 9i45~ƭewD&tG&ݧ2U9 9OLGZ'?]K5ZxL2[ZVͥuSK[ui'2xxbb?G 5EʗܧU&x0~ٮ:R2E=>Љ+"?}LӋ۰iKiۺr90J6f<\b=7ɪMX'] iR IHҌbF]qi7Q yw]: 4iU}TaFz퓧mtX&_+ 03nlh.r IDATh͟X-7ꢼ8gޏyh9/[CmvBX g 3 P٪x.u{~N<>r.>?qrreN(eA'~^t/+Ȉ2\7>.) w[P>I\y}н&:&>I|jl2d_KNSP^F;$γȐ럭L>jyIu*3Z B}ffrgqRZ{㙝5Vʳ]\e5 cŸi2hk %!,wFTC YSYgulcFqR?f1[n5e5Sbk\P~ʥ||y.ʟMu=)zG_y:V[(.;<DehUfd)Ϝ猞 &صEӟZ^ͺc@8*ʗe2yJoZ6/g~GV/ln6kH{Iw7:4MPz8vqDf?Gw~[w>.9T~Î2m^g8%>BlO"Fѭ,h|bp"*ka3Y1E&o_7!ǟ,jJ)NkմE\f{TPAq̥;Ynn`ce-ɬ oȵJϑMd;Id0B:Mc8&XBU&^I^xڮ$ebήO+3,&8yR|~i?Y,t* ;16<)O6Jb*֌>Xs<OJpr)p1Pj9uI94'\iUL&qTr[o+"Mg8* #kf댙裏ʯ~+KC[n]Y3N;>蠃Q~^<Жhfx3B(V",Y`Ҩ)عKHiLu 1m=~=Q"gWME3iіrE}EsR> q\{Ioӥ}^ɨBe{g ?QTެl5Y*{UWbb4n85і2J1[@}-(1蛎(OO,2?B3YN+w>j LzGѮԗ~{dbD[H=oD9g垻ѴK^}cX9AenSgD=}'<B:|\s?߿,G|U|qs_M֧,PAΐ;[(=oiu+P-~&zѷkoCv r4>믥4a^3 d֗ojpaOt|\;_>/8K׍'13g%䚣nF:^q[ޭv,_W's'G]v\sgFrϻѬB޺H:WnUƷ?Hvf~ᰟ}rlcay|iVCYU3BleEKӰEǜӜN%ۈ5mDd`Uw`|mőKlt!4-toa2BY9 J 9 ~xwz1|?ͿXʮxNjx]RZ>\HPg۽}Yޮkֳ m\k6Qc~lu뭷 h]u %? yx~tp[iz)\@s̕Z4ږoN#G.,CL09lFYȱɅ6LpWE?ٵsf;!6MT̖yB^*fʋU㿒&(O\g/G/O>MMyAn?X.̼) ϧ-~,x&9ՁYL*'$ r_V6N;TbZLYHxF(^C4݁8kew")!x;_{_eݤ*O,Cg[媽v/*_= "/#ϓaw[[~y;ޑN9r-^* ]y䎿 O=+CSȽ_w9pΏZmG C5T :iWesONSHCL쥣 zv:SӍb+poHk/ɬo,҃"=oWD 6>Xy`x2'SWGv wg <[A?L ^i=2Iȱisntirn)6-|ne|2«[I1XrOˬGG%tz}NnxUBg* }:FldVcԗռRg,~9 ?}; ZLNDG&wRy4jc9Jh/:O m=RVl11Fq;XM[o݌|8w'42yiό&*?~+EŅGJ{]7|C5eOu"莮:`ϋҧLw\9'Kc7Rh^Y,7?QFYFͱ6 zT*vUO,3߾j rqGHpw7?yV.}+92dt۸}|WGEme̹dg:uvɄd0q*;>2'_;9mTG/z>+sCʯs٧OͥrUf=X<b0^[J뱒E5j0@1 !ب8ro3m.n7+-WoM_ޥJ1ŕvaJ죅VhfKزSqϬ=sd=Xe\Y @GyFP) mt<]o R:ӝ~?#ۥKyS}6̦i'=/by衇3ΐ;@pr<ҫK+D+Y nQО4k>[g0؎n,_N͟(`4?0AS:{YO,ni(g-4tlPS+ NzEK=L-,W~ %˅n+(2Q@`P/cTrk0+_ uez=> idꙛ:H\>2sE\[FTO]o5{{HfQݑQ>]-l6am )Ӎkl2ai/rC} '*v(B&Yixnx/6j*ə;d+C ~^H6?p\-2F|^rilez0χ-1r>kOD trU~QR5Ӝ:wl'ޭQvJx|KmvNq$A'St.K)3OA.vzp@<%`ÒVA.bF"˔Z֟2u@g\AcđnI8YS㞏wF==SG{R PVDd|a*/gfq#]#$=uOg>bT7C}hO(`fv]we0~Oʓ3^ X? G}L6a-a45**KF\4R\<}*d/#Щeehi^P,-7>L{yMs{FG4:/1s)j9cʩ~IsdsdGȅ{Y3W+m2J%C:)Kh+P\.i~ʓ3> X? {* 6F+ZꌂNFϖ復tMoujfx{/\y07kV393eLݧ黥?!w(5W gMֆ :s,4*uAC>OfjQ<)OU"fۈ0".:@iFwHX]^CAXXͦGzS΄؋5nam5ck$_ߊ1Jb; 9K)d֔IV!@_|+9չBN4ϟu!XN.l]Ւ/sZT 6qݩ)O0r`s<@Tk&\|u!奭,,o%5͚7:#gF`.ҍ11 wi*tbzK$.z[\|ϑ#,=F ;Ʌ?ږc;dJ?5X:nG(0E\,o ;Sލ2II|Sy'g9GZj-6FޗE-ɜq/{&/}:jCJB<2syrbe9X;>[g?nMX 0}yFy0i\^C5 Sz%s^MN.~9_GȿnDy@S7:ULSkd#_{ۦ,K:`9rq&Imdez _,_Sb.W{mDi>Hzt<:i|d-vLJ?|pk?GÓgJni `sM6[n/2`xl͒q/)]ӗ/:5'#ߒ&rl2iGnȤnxUoFdҕ7D>AtO;ɠaRC6p4"g׮2i8XXKր;n 6qٝ͟ Vߒ |%YP[&j~JugF|]wzRF 1@c!  fTPň%?dBi=4mYéUJp ب 0wi>5-o9z8:SqUl-/@XGlu崃|›֧.Y`y-Lw}.c=TUoL`9tM4-ltM'J m#[q|Up%Un%>r_N?t:E6 ˺OoaqyEGɎk_iȁ=8I|kKWߣSX~+)#2ړZ ^" # sԓ&۽Nw#/mr/ˢ)&`A][9Q2wɸwZ~c /uM4s_-cҗUĮGJ87o}2ҩO4;Rt ir:Z2D[}ӧ-n^4~))}vy^:Q?L`;0=Rcć*o%zڀYcǬ!kb}zF^Ƭbi䬚iRp$ -YJvگп/mdmڴaW`۷Lm3#;ia}oM @0ӞO:$;s?p۬m_f}awiT"~mQږ?t:ҰˬLs?l:,9l=5km+]~^Shiz)\@E6AU1I~ y؏jJѵZ!7qL-2Mf.e[ʼ˓;alf-->Kk,nt-ϟ>EkоL--*Ӱ)+x4HחJk3.@\ >/E>irU[+V2]B.V*-&_BO'%-4t< V̙/_X54BN_1wLЋ۰ISئQ%ΐbiNfWS>qznTZ7 tT s\_ho@eP+K_ʟ/sG`xhWXpB}f2Kg4lޣ@͗Үckk񓤼a3i*5j; ӛ覕-kiT4hۖ,ZS,yw1/44==aOwӉӜ'p3a0NC,K*dΙ!A+['mtQ y\V{ZIdeh?Mo|Y-!~\w!=z鴬).]QbYQ^_sS>¼Ta_ŠySw}y{Iw)-/WS{F٭o߾rg5\cg ;lJ *S٨2u'c[ Rd6x3ҍh0>E"¿ mi%tccl3.Mok[*]K.:Zt#o€Fکmj X_)4ձ [v1h-q&Һmxn.\ItQڮH"5a|dnXA5oՓzF E`̐;(G5AdbΓO#NAb…tw^|w/+Ž*q+ 7xzנis٨v I[F2I;߰tlt_u<ɿ>׃OΓ=L^Ɣis|IoiK IDATOU0H3Tֹk[{u1&Vi{cvڎybZ"9f5JJJs\\<|BՙrxP,n17HM4{A3ƅ ^ s#$HmMj~̵W%Jv|vϤI wI6l9 8c{9~ƹthixM.՛zpC4f0B *%Ĵ9_!ܲЋշP~hyĩ;YO~QGًaÆ p1;0j|@.onmDav:x˺rlNLN_NX]ZW ,b-e̘BBX=)&Wc~.. b*;Mp˙VF,z O=Ct`Fr#5PF"McJIР>tp:ec$ԏg&n]K3B_~u<9)- K'uW̷@}qѝF<'λBxwZmҋvy5?7_H#eZ tl\f~##.ml$fU(XIrjƘE_i7J_v9iw$__ݔUb~ٛnΦ2]v}p.{2G R*p1)0TךҽLgM6 *%:͆c#F3'ֽI .@uƬMݻXz^@L67Q4gBx4Q"KSX_Qi q-)=Fy:P YYl ڿA}G,D9"!$4L+h0z iLkޭs<32XeZ-\s: W p)/:by<"?-ӌTkA-,}isG ek``ےN'muۚqTĴ̦~MNpעZ11Xj\l. ;}O+ɗ|>֪/sdر68 7^&Mru~a$Y`;FI.]d,] t7Ӭq a1̺~dXq\|[o?z>}Tޅ(% Tg^||pJ[\4g fQކ` kZ&d]Sd>5 Ms kp6R#:Q,V#$W&#uEpSH >'iu)'hH Ktfpxt|@h~e~&'ZK~Ď}ݗRX*1ٽy]v)&R/[>^Y#| ɥ|!eC5Fxg4`]^g1tnݺwQt_}6 \8/=O%W@m,˜Ftnc> !?,f@rD(&uk`^xჽoVi> W7G8q@Xs z8ǵæbX4'_:LiM}, ndۯvy/ׁ JddKAc0!0>.Т˳/E)-LHX<>|v.r,#ÝK %qUQK_E-PmmlUg/ZKXn@lT/E?MZ: }l>ٮq=QoΤGmkNK^8Go>y}iUW c >K.MXkÕ@W@C>62yg؂9}pNI6H;f9'وyYk@- S0kLLƧZ 72ѽse?~+Y`-zCy&Xo>'/-:O$1W-WRJbA4=_.S(=)Zrb({dtذL?r5 4iOj`W Koeh?5EwlmEMnvWZ! Yɨ!Cl/[o~[vm7[S$9~w캴k@0ĵ)T1W|i5ɦLnv6JzYMu㩞4tW^y2^Hi4蠃W^yE]d/SѼz D+Y |wr7Iw9?YBe⸦ЍIQ %s5)Ҁb]ڏ?B P6F omtX[vdݪ@rʋ:4;vI_/X+1\Ӑsdu_ }p3':}Q /6c6³+/M;NѪ';O`"k  zh9zL"G)Τ6,g E펩|k+Ҕby ۷bעgy`=[t`ypfYg(Tہ' En$kDϢ$__0\G'u]F ѕBceW.]H?ݦ|76]<7瞳l4Yն y+X˻).{}ׁ\'_|:&Ns?M󰧱ޗV:P.<^6(gcjBSW'͇0~f,4zh9sl]jKܳtT4h"%کox m`M0`IH0* 1cqPh-c$Ljq@] [h;!aa~(T72JIpCXW#Wɓn*C{ 7j>szNW UR=Ӓ_6ʮ:[E+| Mat|g? | ٮڠ4jȴhl͐ʼnEBI]Jstc*W/g5~3_cgu/B_ôYonnmm('pm6śfv4Lqs8gDC/R!iy|LJ#/d-:uhwLJF:ͤt6|eS; vF;l &X=ħrG]z)Q{mEz85W^ySӫ(PJܐx/H H.wB1 PA]bO {wmݸ}&yw3Μ9;ϔC s뮻.-vtZ4-=Uڞ_>0 >OlF7l\qslw(I8h|)> qQA4Ōi->fE![fH@Ls(a剸2a>k mR`@v\]>2W]uU.b~Z Ƀ챣0f|G B\FNaE\Ȓʧ2Ѣ0ҰΕ:3]\Z*FLeM6-va4eʺW?mRg32|Z?#6uz VzlL[ G9*SO=~{k;tGM5(G.#/֋,:U i3Aq.\ʘq#H, <3GUUt6E]{kI)]QJ^V> bE`ŪT4QKlȃLÜ ю(W8 4<2kv1]uG|fS4t_>Ҏ@^pLl@hiYc4.\f.Z⭀ݜ0{As"͔Onj#`u7%5iBLHC!4Ļﵷi{4H4 <`{ɤl]"`/e:hhf+'B,mVt GN?tkۚZ S^yp?px0gBYZf(0\.LVaeI+Sr mbX)K:E!_r%gV^UW pG;uBMh]Cۍ#$W_ GE;).ʥSwWY8a‰}أÇ ._'6h.ghf8ڦ! HK_Z܊&۟..KˇBy.2p~Q|4ZSQ/ ϦIgSɁ,<r v\yth*6;M}uZq깖6EbNtا^N`dF/pd䖭plpeK&iyq2MzwG2<+,vX5ӓYnR9=7|*Ê4\n$4wpe"}Z^ܝ"r ?Ȇ:_ p!C'æ !@c@Ϗ6HBA;qC~Ȫ ci )vݖ9bjQcZ;# W|DU3iYLpԋ/#2ŧ Rз1e<]L_̗~3w٥&L6x㴨4ѣ8ģe3_Rogg/}K,?1wm2v41;g$*UX6V{ч>uQvRx8]EaO݄j=D2v"(cAA4ZHF e!XY(Bt!f[<1yASFmE FYa33DLF&\3/y^\rd鲩~<2yw_znD*c8G}>UC042CܟhzM.M%oB$ ǥHG2mV+b}.޿j,uKR˯ȷiBC瘒^ad "* tyD0q:63 rL-bګҐM#. Zrn/~pGTw6Ė QD_./ʧUsWKCq5\ *0սO8vyg p!nE^!)ʫl^@ؙ6L`1SR~Gm.fg)f>k R/# a*G؜Ol6fh4Ǒ |m r%=6N@|X:++)_]W\7yN_j:>rDr  +tt,.2세@{R E1饇ҔfꁝB %4Lf 4Ӯ^u[FZ5tP#N ƕso2)"OUai_W+?C5 \K5SlzzӐ\xQFih7h~v۷vE ~|ȸ_ng]~a>K+AK<+ V ]3D'MdjmY[,# Snaxg&{oI~C&3"_*@h G$ӺfҶn8` VaxIkMnʖ?*eUqNZdo6 :311%'g#ٺeJ-n-⹱0?~"}N;EXD͗UD 5WD#Ë/Ű^1 .k1 }-ú,Uſ[җ]LºDyp <,b A0eP$Z򝫶r-^l 4Ŵ=H16~uF 2+ك!jX{ִrf_lӨ![.^i>^kg/c!.-umZ^+^i,aɕDUY4Qs ЦLb)A!wqG7cwgfZ+bM1zܸqF;LL> 2S1*_urS/pNlYf7gۍ#*N:a fx@M?bns!BmʴmeYd6!&"3dM|e䪔iRe1Yv;E/F5o^'.㙃8W2ŲEyR4p!7xIĻɴDxU?NkͤIa)wUMItp@lyX0 j4`#I" ! J#[n;eh,ڛ:mbl? G4}Y99fOi0*WeW W|Wl2hi[eX4h]lS cLY8.[2,/.?Zvm0yd뻙}g4U=CX͇ 5Sy?!v;B|&{ 'رI.L5d f3n*/M[oIٖIQan; 0,/y, =bvmnDv-p$16mq "'Mi#홙2Ng{VJ}"*:e$9*fPJ:UN+/j?Í)) 7:up \9YA6 ѻlvX%H)#l%(Cxݮ*lK7 Pg9IZe(;kSSSw;ºuq& ŗl+^)c2T栕 Y[/Ή:m:D僭\|1z"斺`chic^[kmqq"Ö 3؈Ό+8ir NezjLWa̯*RwB}EsyVD@n1ij3`tvtrnKg .gx^ ?Ø*ޓxTT%` BH 1e3eP/%! ÖVXa3~7@hO:Ǧ=2@縈m_,{ N a.(wq9D8zhӘ ”d?*ɻmc)Ţ FМ55j&kgFSm&V&o^۹ ;Tv6Lޠ=?6cM03&G Ag^?}`ڥAY'a->䐭F\pF8mqtfش3bTk; HFϸiqx r 7-v7 /ȷe?Yw@7 |E*xhg4Զ08:nǟ[lha$,sĻwsOe= 3F+<"B^g--q-#Ī#5(#^:ɦq sh2Zqeyg9?MԄM)D8]J\QfMY|!~C>4}wٴٜË֓8V?v$4hјj3*~3)[锗Ej~Ii>| F*sѠdvBGQhspG{i͵0d9m"M~شR?r=,qmӦ?b 9o8|b8Κ5ęi6:O43mwֵc?Kia&ĈĊ`"16i3Mr0yC 2?Ë^ >2>)ٝ}|VAUi4R dZw8z3aш9b_׾q.>b۵o#.qzTk?-i-'źԊ+~GiS"U C~ی(^vVdB oDM7lbfes~&ԐDi\_>6BkMH5뭑-b4]š%J|dCErTOb\I'8҉HLȃ_Yێ@w @[–CF?i7f^5hPl'i#%́cDD20"IT%K Գ%M$/aͱJNLV} iv&ىχ;pxOVpHξiuvo|ɚs_wYpp z5'9/)?g5wWKoϘ>w[t7-7=U xVڏ*;[8} DZ4h_nd X JeIZJ!LӅ[nik#A&PGuwf+cN4Ɉ-ďi_smҢ+Feݷ9BviǜOuź(u\t,-0.{W4ܘZq6N8KN+?QM\Z?n"l mQx`Ö}!#qh E5ܐN&<ݛqWn>ˈs [.ueҙ/- s^\!Q*jYΦb4e`aqMrV20! `U⎾)EcH|4 y ŏ"gae*yX|~xYk_'ĭBq8"0"$HL1s;H6ṞA[mMvo SL!+h3(6bf.kEse6@tmZձ,T+ o$ NB6U"x{[ZQ޲+Oe>3ކ3無mZ!. d|iS8ތcMv6!bG(#3`ic@3Q҉xi|b$'{|i!+6f\˱A _oF]of6b*1;*O:5|_M-%S0Љ1aOѻEv֮MlDG  #;[T΂,, /ǯǚV޷n3Kl8!n%qh) Cft]v)ǜI̚S/[e(ƕ,qhkZya=W\qcvmkk [of4W?6bwGQ˹g 5`L'co~-2aҺ]vMcaX-Vzqc05جFĊ>8zꩶ#SB6>I#ߍ#]۫mucƟ—b4+g[_$s2qfӫ3us9yИq4V9- <F@\45gd7UnӦ<\r?90qmfNu"eװțoqJ@@4MF`ǯ털 z#!())SlaO zi9JRrDdѯd2i~.A1N8;(Ŭ.R;Zhȑ32yi`ͮYdcgf#<9lwJ6Ub*GV$4_4_mQ AL[Z6"[guj2q ZjpO:.\혰ۄV33'Or"ЌarvQ y>Ъ38FSyp3Fc[nS,6ۅ8i?\DK˥\V:ي_'mS?5N;snC %?!|UW1k-)L)*n)\S3$C(4,uS?oX&,$h92=QqrrHw}mw0p0yd[g .mmmaƌih^k#pK3[-m5/[~.3`na|;M{Tq4bM~v#2 3Yێ{GpaC6C֞w֟)IE F"]EܔnFGk3Έ4_={ ǝ;渹՗?U*o,h^i%Ljn3$j_x,!vڂ q3h#t 2BmqthMYk [̆$-EI1 yԮ4]SGhsOvxa꫍*|,_ve65)_:|k_39o]s4LM.3ԑs/"#Ьkf-3R2-䝵LߖQ/pZE6JQ6OZh8m][~6Waӷ60r!wl%@X0*ᒃ*FbyhZ L6pN[=aS؎G~^?A}az' H)6}B5\­7#v_Y~.lBT{md\,)wM #~E,A6uEJTv1-mN;p7[aÆU ^uzW]!fڀ+I*bӧ!ʍ]?{ۑN'vvmM̆^bcn z4|w}%?hq ihs5;77ʇ6h_|Erb!k6mi>lӈ_:;%W[4l3j OU-ja8)58@h{/-˳)7"% t8aVs)GZ >(r2,chH?G,"nyj@+ qJRwwA \`2~pzMkɵi9F3 A]wuD6Sm5s9>G}MV9YDo&>53G3)Ľk+R `A hY͇H3rL 񲑩qԕɛ5)1XS<בm]Tݎ 9~zOª}&lzza >5!'B"AqH L3f4='_nz>t5l7Fj:}8N#g=PxڟKshs40kzyThmncr1cY37G#kH_7NwcpC jUWLL_H;5o}@{H$J>|i4Ԁ&K;K.ĴېZ&LuAk첫5SE*͆Ȣ8paAr{N^"l\ +'N {C>pf0ӠyƦaޜqY/mmm->?o=cQrێ +"3mKwV>(,x3B9Q6rAX:7^= E Ofţ^Kc_ό>xAᘣsfkfT/1:bbiy@'yG߅0cfZz#]x⋅mwjxMmVafAq  :! c+Bܝ2D;zhBLD$4>E/*đ`ұ>쳶w 7lyz9cǎ !C:]ď{ȹAٸ8 b6_jZf;HR5{aܸqavlFH3oǍb&O$~G#+”տ4(A᳛p5)l3 uqgAn}8+~0a-χo}>:`= tiQz:^Q?'čʻGW1V1?q~c~AީewQc#P!<||גi݆[d0>-`\WoS,7*"~Ghz^|äx_o'a鋝hOV(o3Mƌclv 0abieu>aP1i$[9 7nF]dBsL.g\3/pb-1fCXɜL젍2M Τlk= (|~w65W 7{|XiO7Y!r-TX0C >Nv~TJxo<فny`0xBJ:}@Ly]K0f)\<h  6O넙M$ګ~S[n8 WMr#ЋA&Z3ėf7*N2?p",'Zb:ekĔCY|9ʈ &]liT8c85YVod:1F>*YC4*GTO󭖗&vf4]LF# jԨQUcB΀h8VwխaK& s#{¤?_TXartIDATbq2{ɸDH`NLFXbjW C6j}d#v|t;\h\@|(Xdv̏8@gVf Č3Cfᄸ\YB,r4TTKy3@d)roAn^6J y~k: ?OO~"o|#Mְ,i@ͮ￿V͋ /#>*AC3~' _^^: 2Nl} Hw % <37j%q4Q52h3y֓ƥn],xv h@9͠thy{o#MNӡg#SN9vE4y_]tQ+ǘ[[{"˔MvfZt:0"/~9c=k/Xe]+N2n;#4}+}2.ȅ Yχ #[vA@vjEc`liE)Ci)O2Nێ#0O(F;jy(\..Z<fmfFAb9xM7UM F{QG[nibօ^;<#ȓZp Yem-SF\Ooe)^Vin3 *SUK,OsGog΃dZΕ:؂k0N;ΠiG?:˴.[EeW'2(ptG#uJOh@фkUW]eX!$=k=0`&[k6po=B\S+Nʶbd&.Gie,U>n;#t`G#T3Lύ#Ud-bCӟ:["Gn iuJeLt;Ӧ81¦Os1gQC=docVYe|D@~/bAc6h0uTps~1R-\׋\-:G(YL4NETFnGprcAxE@p֏Rwy4 Nq\w}#E^˧UYÆ ]٠l*a>}UJټ&hW[m5ۑݕ&krSn,/PS^H^,#+mLʕC#8G&qLGU2wAO8U(vffE]L[+([ϏͶ{pWؑCf1S!cƌ ~[PR,G: i|ŧvGpG""w0E Ŵ?uy*\6qrˮ|R95jK[Ze.b᳟l-+Swe+'< KJ#Z\p]&n.]Z|*nGݷw!P~(bv> ACGpg45zGy7pGpGpG8!7/pGpGpGy7pz51ܟ'{׫/pGw"m{U9@gpB<#[?,2_eW8#{Lﶢ0kt8!: !7hР0~^_#8@C1gzaw~E# NAe^ cnW8#{mz׉krpB8V.j~oM6 ׿¸qz9#.Ǝ^~{lx9]گ Nuz " AEY$|_ #G N=V4@\pz;))fx6B:똖x /b0`@oïpG`>EW^ vuW B̻Mx><#PaXtE[om_?\qezGpz9X3;kȐ!vb~cqX{9,~y#P>FG9@@࣏> g3g ~a¿wy'L>lO?t0am6lFT~#8=Yfɓ''1cXͶn^:l^Zr%/[l1#|Ƹ]Wp1N1^#؜9s 13fTHom{Ys=fpލ^?z}sa%0RK !ft#mį!;}Z; 99S8=F,t5\3jF' 鯏@8#0H8ks/Z`~ciyI#iNMb~zB@X`.?Tc}1dE)pGh)]rwpy/0ZC,KlE=GpB<3#r4 B+B 뭈cpf1SEEUIr8#twR^KK 2 u]]8!]ӯ2"  2bƀpbȰÐap+Gp R̻*}_&iS7q$PwpzN{Ͻ+q !fv2ObҦZԭvGpE@dX+l>J{ cܼ{:#>{Wt 2 D c` k};n;#-FTKzONIsJ[QpЯh9逃Ӂ&jҠpG]nt#v8bo# hvSR  fqGh1zG'*Qkqu<;GX \} zDA_")& #9Y/pG" -6aa#N_#TC q5d<p }3Ö+Rpő(u;#8@)ƆbMV9@A q﹗~%@# +spGh5"?F+mGp!28uA+#8@7"^oo_#Н;GpGplEw;#8#8#8= '=xuGpGpGp N ^#8#8#8@C q!^GpGpGpyy8#8#8#pBnWpGpGpG` x8#8#8#0qGpGpG78!78{)#8#8#8= '=xuGpGpGp ܇2nEIENDB`amqp-1.8.0/.travis.yml0000644000004100000410000000065613321132064014621 0ustar www-datawww-datalanguage: ruby bundler_args: --without development before_script: ./bin/ci/before_build script: "bundle exec rake spec:ci" env: - CI=true rvm: - 2.3.3 - 2.4.1 gemfile: - Gemfile - gemfiles/eventmachine-pre notifications: recipients: - michael@rabbitmq.com branches: only: - master - 1.7.x-stable matrix: allow_failures: - rvm: jruby gemfile: gemfiles/eventmachine-pre services: - rabbitmq amqp-1.8.0/.rspec0000644000004100000410000000003213321132064013611 0ustar www-datawww-data--color --format progress amqp-1.8.0/README.md0000644000004100000410000002313213321132064013761 0ustar www-datawww-data# Ruby amqp gem: the asynchronous Ruby RabbitMQ client [Ruby amqp gem](http://rubyamqp.info) is a feature-rich, EventMachine-based RabbitMQ client with batteries included. It's the original RabbitMQ client for Ruby. These days there are very solid alternatives available: [Bunny](http://rubybunny.info) for MRI and [March Hare](http://rubymarchhare.info) for JRuby. It implement [AMQP 0.9.1](http://www.rabbitmq.com/tutorials/amqp-concepts.html) and support [RabbitMQ extensions to AMQP 0.9.1](http://www.rabbitmq.com/extensions.html). ## A Word of Warning: Use This Only If You Already Use EventMachine Unless you **already use EventMachine**, there is no real reason to use this client. Consider [Bunny](http://rubybunny.info) or [March Hare](http://rubymarchhare.info) instead. amqp gem brings in a fair share of EventMachine complexity which cannot be fully eliminated. Event loop blocking, writes that happen at the end of loop tick, uncaught exceptions in event loop silently killing it: it's not worth the pain unless you've already deeply invested in EventMachine and understand how it works. So, just use Bunny or March Hare. You will be much happier. ## I know what RabbitMQ is, how do I get started? See [Getting started with amqp gem](http://rubyamqp.info/articles/getting_started/) and other [amqp gem documentation guides](http://rubyamqp.info/). We recommend that you read [AMQP 0.9.1 Model Explained](http://www.rabbitmq.com/tutorials/amqp-concepts.html), too. ## What is RabbitMQ? RabbitMQ is an open source messaging middleware that emphasizes interoperability between different technologies (for example, Java, .NET, Ruby, Python, Node.js, Erlang, Go, C and so on). Key features of RabbitMQ are very flexible yet simple routing and binary protocol efficiency. RabbitMQ supports many sophisticated features, for example, message acknowledgements, queue length limit, message TTL, redelivery of messages that couldn't be processed, load balancing between message consumers and so on. ## What is amqp gem good for? One can use amqp gem to make Ruby applications interoperate with other applications (both Ruby and not). Complexity and size may vary from simple work queues to complex multi-stage data processing workflows that involve dozens or hundreds of applications built with all kinds of technologies. Specific examples: * Events collectors, metrics & analytics applications can aggregate events produced by various applications (Web and not) in the company network. * A Web application may route messages to a Java app that works with SMS delivery gateways. * MMO games can use flexible routing AMQP provides to propagate event notifications to players and locations. * Price updates from public markets or other sources can be distributed between interested parties, from trading systems to points of sale in a specific geographic region. * Content aggregators may update full-text search and geospatial search indexes by delegating actual indexing work to other applications over AMQP. * Companies may provide streaming/push APIs to their customers, partners or just general public. * Continuous integration systems can distribute builds between multiple machines with various hardware and software configurations using advanced routing features of AMQP. * An application that watches updates from a real-time stream (be it markets data or Twitter stream) can propagate updates to interested parties, including Web applications that display that information in the real time. ## Getting started with Ruby amqp gem ### Install RabbitMQ Please refer to the [RabbitMQ installation guide](http://www.rabbitmq.com/install.html). Note that for Ubuntu and Debian we strongly advice that you use [RabbitMQ apt repository](http://www.rabbitmq.com/debian.html#apt) that has recent versions of RabbitMQ. RabbitMQ packages Ubuntu and Debian ship with are outdated even in recent (10.10) releases. Learn more in the [RabbitMQ versions guide](http://rubydoc.info/github/ruby-amqp/amqp/master/file/docs/RabbitMQVersions.textile). ### Install the gem On Microsoft Windows 7 gem install eventmachine gem install amqp On other OSes gem install amqp ### "Hello, World" example ``` ruby #!/usr/bin/env ruby # encoding: utf-8 require 'amqp' EventMachine.run do connection = AMQP.connect(:host => '127.0.0.1') puts "Connecting to RabbitMQ. Running #{AMQP::VERSION} version of the gem..." ch = AMQP::Channel.new(connection) q = ch.queue("amqpgem.examples.hello_world", :auto_delete => true) x = ch.default_exchange q.subscribe do |metadata, payload| puts "Received a message: #{payload}. Disconnecting..." connection.close { EventMachine.stop { exit } } end x.publish "Hello, world!", :routing_key => q.name end ``` [Getting started guide](http://rubyamqp.info/articles/getting_started/) explains this and two more examples in detail, and is written in a form of a tutorial. See [AMQP 0.9.1 Model Explained](http://www.rabbitmq.com/tutorials/amqp-concepts.html) if you want to learn more about RabbitMQ protocol principles & concepts. ## Supported Ruby Versions amqp gem `1.6.x` and `1.7.x` series support * Ruby 2.4 * Ruby 2.3 * Ruby 2.2 * Ruby 2.1 * Ruby 2.0 amqp gem `1.5.x` series also supports * Ruby 1.9 JRuby users should use [March Hare](http://rubymarchhare.info/). ## Documentation: tutorials, guides & API reference We believe that in order to be a library our users **really** love, we need to care about documentation as much as (or more) code readability, API beauty and autotomated testing across 5 Ruby implementations on multiple operating systems. We do care about our [documentation](http://rubyamqp.info): **if you don't find your answer in documentation, we consider it a high severity bug** that you should [file to us](http://github.com/ruby-amqp/amqp/issues). Or just complain to [@rubyamqp](https://twitter.com/rubyamqp) on Twitter. ### Tutorials [Getting started guide](http://rubyamqp.info/articles/getting_started/) is written as a tutorial that walks you through 3 examples: * The "Hello, world" of messaging, 1-to-1 communication * Blabbr, a Twitter-like example of broadcasting (1-to-many communication) * Weathr, an example of sophisticated routing capabilities AMQP 0.9.1 has to offer (1-to-many or many-to-many communication) all in under 20 minutes. [AMQP 0.9.1 Concepts](http://www.rabbitmq.com/tutorials/amqp-concepts.html) will introduce you to protocol concepts in less than 5 minutes. ### Guides [Documentation guides](http://rubyamqp.info) describe the library itself as well as AMQP concepts, usage scenarios, topics like working with exchanges and queues, error handing & recovery, broker-specific extensions, TLS support, troubleshooting and so on. Most of the documentation is in these guides. ### Examples You can find many examples (both real-world cases and simple demonstrations) under [examples directory](https://github.com/ruby-amqp/amqp/tree/master/examples) in the repository. Note that those examples are written against version 0.8.0.rc1 and later. 0.6.x and 0.7.x may not support certain AMQP protocol or "DSL syntax" features. There is also a work-in-progress [Messaging Patterns and Use Cases With AMQP](http://rubyamqp.info/articles/patterns_and_use_cases/) documentation guide. ### API reference [API reference](http://bit.ly/mDm1JE) is up on [rubydoc.info](http://rubydoc.info) and is updated daily. ## How to use AMQP gem with Ruby on Rails, Sinatra and other Web frameworks We cover Web application integration for multiple Ruby Web servers in [Connecting to the broker guide](http://rubyamqp.info/articles/connecting_to_broker/). ## Community * Join also [RabbitMQ mailing list](https://groups.google.com/forum/#!forum/rabbitmq-users) (the AMQP community epicenter). * Join [Ruby AMQP mailing list](http://groups.google.com/group/ruby-amqp) * Follow [@rubyamqp](https://twitter.com/rubyamqp) on Twitter for Ruby AMQP ecosystem updates. * Stop by #rabbitmq on irc.freenode.net. You can use [Web IRC client](http://webchat.freenode.net?channels=rabbitmq) if you don't have IRC client installed. ## Maintainer Information amqp gem is maintained by [Michael Klishin](http://twitter.com/michaelklishin). ## Continuous Integration [![Continuous Integration status](https://secure.travis-ci.org/ruby-amqp/amqp.png?branch=master)](http://travis-ci.org/ruby-amqp/amqp) ## Links ## * [API reference](http://rdoc.info/github/ruby-amqp/amqp/master/frames) * [Documentation guides](http://rubyamqp.info) * [Code Examples](https://github.com/ruby-amqp/amqp/tree/master/examples) * [Issue tracker](http://github.com/ruby-amqp/amqp/issues) * [Continous integration status](http://travis-ci.org/#!/ruby-amqp/amqp) ## License ## amqp gem is licensed under the [Ruby License](http://www.ruby-lang.org/en/LICENSE.txt). ## Credits and copyright information ## * The Original Code is [tmm1/amqp](http://github.com/tmm1/amqp). * The Initial Developer of the Original Code is Aman Gupta. * Copyright (c) 2008 - 2010 [Aman Gupta](http://github.com/tmm1). * Contributions from [Jakub Stastny](http://github.com/botanicus) are Copyright (c) 2011-2012 VMware, Inc. * Copyright (c) 2010 — 2017 [ruby-amqp](https://github.com/ruby-amqp) group members. Currently maintained by [ruby-amqp](https://github.com/ruby-amqp) group members Special thanks to Dmitriy Samovskiy, Ben Hood and Tony Garnock-Jones. ## How can I learn more about RabbitMQ and messaging in general? ### Documentation Resources * [AMQP 0.9.1 Model Explained](http://www.rabbitmq.com/tutorials/amqp-concepts.html) * [RabbitMQ tutorials](http://www.rabbitmq.com/getstarted.html) that demonstrate interoperability amqp-1.8.0/bin/0000755000004100000410000000000013321132064013251 5ustar www-datawww-dataamqp-1.8.0/bin/irb0000755000004100000410000000022613321132064013753 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 $LOAD_PATH.unshift(File.expand_path("../../lib", __FILE__)) require "irb" require "amqp" IRB.start(__FILE__) amqp-1.8.0/bin/docup0000755000004100000410000000005013321132064014304 0ustar www-datawww-data#!/usr/bin/env sh yard server --reload amqp-1.8.0/bin/ci/0000755000004100000410000000000013321132064013644 5ustar www-datawww-dataamqp-1.8.0/bin/ci/before_build0000755000004100000410000000334013321132064016213 0ustar www-datawww-data#!/usr/bin/env ruby $ctl = ENV["AMQP_GEM_RABBITMQCTL"] || ENV["RABBITMQCTL"] || "sudo rabbitmqctl" $plugins = ENV["AMQP_GEM_RABBITMQ_PLUGINS"] || ENV["RABBITMQ_PLUGINS"] || "sudo rabbitmq-plugins" def rabbit_control(args) command = "#{$ctl} #{args}" system command end def rabbit_plugins(args) command = "#{$plugins} #{args}" system command end # guest:guest has full access to / rabbit_control 'add_vhost /' rabbit_control 'add_user guest guest' rabbit_control 'set_permissions -p / guest ".*" ".*" ".*"' # amqp_gem:amqp_gem_password has full access to amqp_gem_testbed rabbit_control 'add_vhost amqp_gem_testbed' rabbit_control 'add_user amqp_gem amqp_gem_password' rabbit_control 'set_permissions -p amqp_gem_testbed amqp_gem ".*" ".*" ".*"' # amqp_gem_reader:reader_password has read access to amqp_gem_testbed rabbit_control 'add_user amqp_gem_reader reader_password' rabbit_control 'clear_permissions -p amqp_gem_testbed guest' rabbit_control 'set_permissions -p amqp_gem_testbed amqp_gem_reader "^---$" "^---$" ".*"' # requires RabbitMQ 3.0+ # $RABBITMQ_PLUGINS enable rabbitmq_consistent_hash_exchange # Reduce retention policy for faster publishing of stats rabbit_control "eval 'supervisor2:terminate_child(rabbit_mgmt_sup_sup, rabbit_mgmt_sup), application:set_env(rabbitmq_management, sample_retention_policies, [{global, [{605, 1}]}, {basic, [{605, 1}]}, {detailed, [{10, 1}]}]), rabbit_mgmt_sup_sup:start_child().'" rabbit_control "eval 'supervisor2:terminate_child(rabbit_mgmt_agent_sup_sup, rabbit_mgmt_agent_sup), application:set_env(rabbitmq_management_agent, sample_retention_policies, [{global, [{605, 1}]}, {basic, [{605, 1}]}, {detailed, [{10, 1}]}]), rabbit_mgmt_agent_sup_sup:start_child().'" amqp-1.8.0/bin/cleanify.rb0000755000004100000410000000172613321132064015401 0ustar www-datawww-data#!/usr/bin/env ruby -i # encoding: utf-8 # Usage: # find . | egrep '\.rb$' | egrep -v cleanify.rb | xargs ./bin/cleanify.rb # \n at the end of the file # def foo a, b, &block # no trailing whitespace # encoding declaration ENCODING = "utf-8" while line = ARGF.gets # whitespace # line.chomp! line.gsub!(/\t/, " ") line.rstrip! # encoding if line.length == (ARGF.pos - 1) && ! line.match(/^#.*coding/) puts "# encoding: #{ENCODING}\n\n" end # def regexp = /^(\s*def \w[\w\d]*)\s+(.+)$/ if line.match(regexp) line.gsub!(regexp, '\1(\2)') end # foo{} => foo {} line.gsub!(/([^%][^#( ])(\{)/, '\1 \2') # a=foo => a = foo line.gsub!(/([^ ])(\+=)/, '\1 \2') line.gsub!(/(\+=)([^ ])/, '\1 \2') line.gsub!(/([^ :])(<<)/, '\1 \2') line.gsub!(/(<<)([^ ])/, '\1 \2') # foo=>bar line.gsub!(/([^\s])=>/, '\1 =>') line.gsub!(/=>([^\s])/, '=> \1') line.gsub!(/\{\|/, '{ |') line.gsub!(/,\s*/, ', ') line.rstrip! puts line end amqp-1.8.0/bin/set_test_suite_realms_up.sh0000755000004100000410000000121313321132064020717 0ustar www-datawww-data#!/bin/sh # guest:guest has full access to / rabbitmqctl add_vhost / rabbitmqctl add_user guest guest rabbitmqctl set_permissions -p / guest ".*" ".*" ".*" # amqp_gem:amqp_gem_password has full access to amqp_gem_testbed rabbitmqctl add_vhost amqp_gem_testbed rabbitmqctl add_user amqp_gem amqp_gem_password rabbitmqctl set_permissions -p amqp_gem_testbed amqp_gem ".*" ".*" ".*" # amqp_gem_reader:reader_password has read access to amqp_gem_testbed rabbitmqctl add_user amqp_gem_reader reader_password rabbitmqctl clear_permissions -p amqp_gem_testbed guest rabbitmqctl set_permissions -p amqp_gem_testbed amqp_gem_reader "^---$" "^---$" ".*"amqp-1.8.0/spec/0000755000004100000410000000000013321132064013433 5ustar www-datawww-dataamqp-1.8.0/spec/unit/0000755000004100000410000000000013321132064014412 5ustar www-datawww-dataamqp-1.8.0/spec/unit/amqp/0000755000004100000410000000000013321132064015350 5ustar www-datawww-dataamqp-1.8.0/spec/unit/amqp/connection_spec.rb0000644000004100000410000000402013321132064021042 0ustar www-datawww-data# encoding: utf-8 require 'spec_helper' require 'amqp' describe AMQP do # # Examples # it "has default settings" do s = AMQP.settings.dup s[:host].should == "127.0.0.1" s[:port].should == 5672 s[:user].should == "guest" s[:pass].should == "guest" s[:heartbeat].should be_nil s[:auth_mechanism].should eq([]) end describe "connection to RabbitMQ with a connection string" do include EventedSpec::SpecHelper em_before { AMQP.cleanup_state } em_after { AMQP.cleanup_state } it 'parses URI string' do em do AMQP.start("amqp://guest:guest@127.0.0.1?heartbeat=10&connection_timeout=100") do |session| expect(session.heartbeat_interval).to eq(10) expect(session.connection_timeout).to eq(100) session.close end done(0.3) end end end describe '.start' do # # Environment # include EventedSpec::SpecHelper em_before { AMQP.cleanup_state } em_after { AMQP.cleanup_state } # # Examples # it 'yields to given block AFTER connection is established' do em do AMQP.start AMQP_OPTS do @block_fired = true AMQP.connection.should be_connected end done(0.3) { @block_fired.should be_true } end end it 'should try to connect again in case previous conection failed' do em do timeout(20) error_handler = proc { EM.next_tick { AMQP.start(AMQP_OPTS) { done } } } # Assuming that you don't run your amqp @ port 65535 AMQP.start(AMQP_OPTS.merge(:port => 65535, :on_tcp_connection_failure => error_handler)) end end it 'should keep connection if there was no failure' do em do error_handler = proc {} @block_fired_times = 0 AMQP.start(AMQP_OPTS) { @block_fired_times += 1 } delayed(0.1) { AMQP.start(AMQP_OPTS) { @block_fired_times += 1 } } done(0.3) { @block_fired_times.should == 1 } end end end # .start end # describe AMQP amqp-1.8.0/spec/unit/amqp/channel_id_allocation_spec.rb0000644000004100000410000000265513321132064023210 0ustar www-datawww-data# encoding: utf-8 require "spec_helper" describe AMQP::ChannelIdAllocator do class ChannelAllocator include AMQP::ChannelIdAllocator end describe "#next_channel_id" do subject do ChannelAllocator.new end context "when there is a channel id available for allocation" do it "returns that channel id" do 1024.times { subject.next_channel_id } subject.next_channel_id.should == 1025 end end context "when THERE IS NO a channel id available for allocation" do it "raises an exception" do (ChannelAllocator::MAX_CHANNELS_PER_CONNECTION - 1).times do subject.next_channel_id end lambda { subject.next_channel_id }.should raise_error end end end describe ".release_channel_id" do subject do ChannelAllocator.new end it "releases that channel id" do 1024.times { subject.next_channel_id } subject.next_channel_id.should == 1025 subject.release_channel_id(128) subject.next_channel_id.should == 128 subject.next_channel_id.should == 1026 end end describe "each instance gets its own channel IDs" do it "has an allocator per instance" do one = ChannelAllocator.new two = ChannelAllocator.new one.next_channel_id.should == 1 one.next_channel_id.should == 2 two.next_channel_id.should == 1 two.next_channel_id.should == 2 end end end amqp-1.8.0/spec/spec_helper.rb0000644000004100000410000000365613321132064016263 0ustar www-datawww-data# encoding: utf-8 $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__) require 'bundler' Bundler.setup(:default, :test) require "amqp" require "evented-spec" require "effin_utf8" require "multi_json" puts "Using Ruby #{RUBY_VERSION} and amq-protocol #{AMQ::Protocol::VERSION}" amqp_config = File.dirname(__FILE__) + '/amqp.yml' port = if ENV["TRACER"] 5673 else 5672 end if File.exists? amqp_config class Hash def symbolize_keys self.inject({}) do |result, (key, value)| new_key = key.is_a?(String) ? key.to_sym : key new_value = value.is_a?(Hash) ? value.symbolize_keys : value result[new_key] = new_value result end end end AMQP_OPTS = YAML::load_file(amqp_config).symbolize_keys[:test] else AMQP_OPTS = {:host => 'localhost', :port => port} end # puts "AMQP_OPTS = #{AMQP_OPTS.inspect}" # # Ruby version-specific # case RUBY_VERSION when "1.8.7" then class Array alias sample choice end when "1.8.6" then raise "Ruby 1.8.6 is not supported. Sorry, pal. Time to move on beyond One True Ruby. Yes, time flies by." end EventMachine.kqueue = true if EventMachine.kqueue? EventMachine.epoll = true if EventMachine.epoll? module RabbitMQ module Control def rabbitmq_pid $1.to_i if `rabbitmqctl status` =~ /\{pid,(\d+)\}/ end def start_rabbitmq(delay = 1.0) # this is Homebrew-specific :( `rabbitmq-server > /dev/null 2>&1 &`; sleep(delay) end def stop_rabbitmq(pid = rabbitmq_pid, delay = 1.0) `rabbitmqctl stop`; sleep(delay) end def kill_rabbitmq(pid = rabbitmq_pid, delay = 1.0) # tango is down, tango is down! Process.kill("KILL", pid); sleep(delay) end end end module PlatformDetection def mri? !defined?(RUBY_ENGINE) || (defined?(RUBY_ENGINE) && ("ruby" == RUBY_ENGINE)) end def rubinius? defined?(RUBY_ENGINE) && (RUBY_ENGINE == 'rbx') end end amqp-1.8.0/spec/integration/0000755000004100000410000000000013321132064015756 5ustar www-datawww-dataamqp-1.8.0/spec/integration/basic_publish_with_message_framing_spec.rb0000644000004100000410000000325113321132064026367 0ustar www-datawww-data# encoding: utf-8 require 'spec_helper' describe "Large messages that need to be framed" do # # Environment # include EventedSpec::AMQPSpec default_timeout 10 amqp_before do @channel = AMQP::Channel.new @channel.should be_open @channel.on_error do |ch, close| raise "Channel-level error!: #{close.inspect}" end @queue1 = @channel.queue("amqpgem.tests.integration.queue#{Time.now.to_i}#{rand}", :exclusive => true) @queue2 = @channel.queue("amqpgem.tests.integration.queue#{Time.now.to_i}#{rand}", :exclusive => true) # Rely on default direct exchange binding, see section 2.1.2.4 Automatic Mode in AMQP 0.9.1 spec. @exchange = AMQP::Exchange.default(@channel) end default_options AMQP_OPTS let(:body) { "мультибайт" * 1024 * 1024 } # # Examples # it "are framed correctly" do @exchange.channel.should == @channel number_of_received_messages = 0 expected_number_of_messages = 3 dispatched_data = "to be received by queue1" @queue1.subscribe do |payload| number_of_received_messages += 1 payload.should == dispatched_data end # subscribe 4.times do @exchange.publish(body, :routing_key => "killa key") end expected_number_of_messages.times do @exchange.publish(dispatched_data, :routing_key => @queue1.name) end 4.times do @exchange.publish(body, :routing_key => "killa key") end delayed(0.6) { # We never subscribe to it, hence, need to delete manually @queue2.delete } done(5.0) { number_of_received_messages.should == expected_number_of_messages } end # it end # describe amqp-1.8.0/spec/integration/regressions/0000755000004100000410000000000013321132064020321 5ustar www-datawww-dataamqp-1.8.0/spec/integration/regressions/issue66_spec.rb0000644000004100000410000000051513321132064023165 0ustar www-datawww-data# encoding: utf-8 require "spec_helper" describe "Immediate disconnection" do # # Environment # include EventedSpec::AMQPSpec include EventedSpec::SpecHelper after :all do done end it "succeeds" do c = AMQP.connect c.disconnect { done } end # it end # describe "Authentication attempt" amqp-1.8.0/spec/integration/regressions/empty_message_body_spec.rb0000644000004100000410000000244013321132064025537 0ustar www-datawww-data# encoding: utf-8 require "spec_helper" describe "Messages with empty bodies" do # # Environment # include EventedSpec::AMQPSpec default_options AMQP_OPTS default_timeout 5 amqp_before do @channel = AMQP::Channel.new @channel.on_error do |ch, close| raise "Channel-level error!: #{close.inspect}" end @queue = @channel.queue("", :auto_delete => true) @exchange = @channel.direct("amqpgem.tests.integration.direct.exchange", :auto_delete => true) @queue.bind(@exchange, :routing_key => "builds.all") end it "can be mixed with any other messages" do mailbox1 = Array.new mailbox2 = Array.new consumer1 = AMQP::Consumer.new(@channel, @queue).consume consumer2 = AMQP::Consumer.new(@channel, @queue).consume consumer1.on_delivery do |metadata, payload| mailbox1 << payload end consumer2.on_delivery do |metadata, payload| mailbox2 << payload end EventMachine.add_timer(0.5) do 12.times { @exchange.publish("", :routing_key => "builds.all") } 12.times { @exchange.publish(".", :routing_key => "all.builds") } 12.times { @exchange.publish("", :routing_key => "all.builds") } end done(1.5) { mailbox1.size.should == 6 mailbox2.size.should == 6 } end end amqp-1.8.0/spec/integration/regressions/concurrent_publishing_on_the_same_channel_spec.rb0000644000004100000410000000771313321132064032327 0ustar www-datawww-data# encoding: utf-8 require "spec_helper" include PlatformDetection # Only run this for MRI. # # rubinius implementation of ThreadGroup has a bug: it references objects without checking # whether they are alive. So sandbox this test for other Rubies for now. Per discussion with # brixen in #travis, see also https://gist.github.com/1100572. # # JRuby is having weird CI issues, too. Still investigating them. MK. if mri? describe "Concurrent publishing on a shared channel from multiple threads" do # # Environment # include EventedSpec::AMQPSpec default_options AMQP_OPTS default_timeout 15 amqp_before do @channel = AMQP::Channel.new @channel.on_error do |ch, close| raise "Channel-level error!: #{close.inspect}" end @channel.connection.on_error do |conn, connection_close| raise "Handling a connection-level exception: #{connection_close.reply_text}" end end let(:inputs) do [ { :index=>{:_routing=>530,:_index=>"optimizer",:_type=>"earnings",:_id=>530}}, { :total_conversions=>0,:banked_clicks=>0,:total_earnings=>0,:pending_conversions=>0,:paid_net_earnings=>0,:banked_conversions=>0,:pending_earnings=>0,:optimizer_id=>530,:total_impressions=>0,:banked_earnings=>0,:bounce_count=>0,:time_on_page=>0,:total_clicks=>0,:entrances=>0,:pending_clicks=>0,:paid_earnings=>0}, { :index=>{:_routing=>430,:_index=>"optimizer",:_type=>"earnings",:_id=>430}}, { :total_conversions=>1443,:banked_clicks=>882,:total_earnings=>5796.3315841537,:pending_conversions=>22,:paid_net_earnings=>4116.90224486802,:banked_conversions=>1086,:pending_earnings=>257.502767857143,:optimizer_id=>430,:total_impressions=>6370497,:banked_earnings=>122.139339285714,:bounce_count=>6825,:time_on_page=>0,:total_clicks=>38143,:entrances=>12336,:pending_clicks=>1528,:paid_earnings=>5670.78224486798}, { :index=>{:_routing=>506,:_index=>"optimizer",:_type=>"earnings",:_id=>506}}, { :total_conversions=>237,:banked_clicks=>232,:total_earnings=>550.6212071428588277,:pending_conversions=>9,:paid_net_earnings=>388.021207142857,:banked_conversions=>225,:pending_earnings=>150.91,:optimizer_id=>506,:total_impressions=>348319,:banked_earnings=>12.92,:bounce_count=>905,:time_on_page=>0,:total_clicks=>4854,:entrances=>1614,:pending_clicks=>1034,:paid_earnings=>537.501207142858}, {:index=>{:_routing=>345,:_index=>"optimizer",:_type=>"earnings",:_id=>345}}, {:total_conversions=>0,:banked_clicks=>0,:total_earnings=>0,:pending_conversions=>0,:paid_net_earnings=>0,:banked_conversions=>0,:pending_earnings=>0,:optimizer_id=>345,:total_impressions=>0,:banked_earnings=>0,:bounce_count=>0,:time_on_page=>0,:total_clicks=>0,:entrances=>0,:pending_clicks=>0,:paid_earnings=>0} ] end let(:messages) { inputs.map {|i| MultiJson.encode(i) } * 3 } # # Examples # it "DOES NOT result in frames being delivered out of order (no UNEXPECTED_FRAME connection exceptions)" do received_messages = [] queue = @channel.queue("amqpgem.tests.concurrent_publishing", :auto_delete => true) exchange = @channel.default_exchange exchange.on_return do |method, header, body| puts "Message was returned: #{method.reply_text}" end queue.subscribe do |metadata, payload| received_messages << payload end EventMachine.add_timer(2.0) do # ZOMG THREADS! 20.times do Thread.new do messages.each do |message| exchange.publish(message, :routing_key => queue.name, :mandatory => true) end end end end # let it run for several seconds because you know, concurrency issues do not always manifest themselves # immediately. MK. done(14.0) { # we don't care about the exact number of messages sent or received, just the fact that there are # no UNEXPECTED_FRAME connection-level exceptions. MK. } end end end amqp-1.8.0/spec/integration/queue_redeclaration_with_incompatible_attributes_spec.rb0000644000004100000410000000353313321132064031370 0ustar www-datawww-data# encoding: utf-8 require 'spec_helper' describe "AMQP queue redeclaration with different attributes" do # # Environment # include EventedSpec::AMQPSpec default_timeout 5 # # Examples # context "when :durable value is different" do let(:name) { "amqp-gem.nondurable.queue" } let(:options) { { :durable => false, :exclusive => true, :auto_delete => true, :arguments => {}, :passive => false } } let(:different_options) { { :durable => true, :exclusive => true, :auto_delete => true, :arguments => {}, :passive => false} } it "should raise AMQP::IncompatibleOptionsError for incompatable options" do channel = AMQP::Channel.new channel.on_error do |ch, channel_close| @error_code = channel_close.reply_code end channel.queue(name, options) expect { channel.queue(name, different_options) }.to raise_error done(0.5) { @error_code.should == 406 } end end context "when :headers are different" do let(:name) { "amqp-gem.nondurable.queue" } let(:options) { { :durable => false, :exclusive => true, :auto_delete => true, :arguments => {}, :passive => false } } let(:different_options) { { :durable => false, :exclusive => true, :auto_delete => true, :arguments => {}, :passive => false, :header => {:random => 'stuff' } } } it "should not raise AMQP::IncompatibleOptionsError for irrelevant options" do channel = AMQP::Channel.new channel.on_error do |ch, channel_close| @error_code = channel_close.reply_code end channel.queue(name, options) expect { channel.queue(name, different_options) }.to_not raise_error done(0.5) { @error_code.should == 406 } end end end # describe AMQP amqp-1.8.0/spec/integration/basic_return_spec.rb0000644000004100000410000000174413321132064022003 0ustar www-datawww-data# encoding: utf-8 require "spec_helper" describe "Message published as mandatory" do # # Environment # include EventedSpec::AMQPSpec include EventedSpec::SpecHelper em_before { AMQP.cleanup_state } em_after { AMQP.cleanup_state } default_options AMQP_OPTS default_timeout 3 amqp_before do @channel = AMQP::Channel.new @channel.should be_open @exchange = @channel.fanout("amqpgem.specs.#{Time.now.to_i}", :auto_delete => true, :durable => false) end after(:all) do AMQP.cleanup_state done end context "that cannot be routed to any queue" do it "is returned to the publisher via basic.return" do returned_messages = [] @exchange.on_return do |basic_return, header, body| returned_messages << basic_return.reply_text end (1..10).to_a.each { |m| @exchange.publish(m, :mandatory => true) } done(1.0) { returned_messages.should == Array.new(10) { "NO_ROUTE" } } end end end amqp-1.8.0/spec/integration/message_metadata_access_spec.rb0000644000004100000410000000637313321132064024133 0ustar www-datawww-data# encoding: utf-8 require 'spec_helper' describe "Message attributes" do # # Environment # include EventedSpec::AMQPSpec default_timeout 2 amqp_before do @connection = AMQP.connect @channel = AMQP::Channel.new(@connection) end after(:all) do AMQP.cleanup_state done end # # Examples # it "can be accessed in a unified manner (basic.delivery attributes + message attributes)" do queue = @channel.queue("amqpgem.tests.metadata_access", :auto_delete => true) exchange = @channel.direct("amq.direct") queue.bind(exchange, :routing_key => "amqpgem.key") @channel.on_error do |ch, channel_close| fail(channel_close.reply_text) @connection.close { EventMachine.stop } end @now = Time.now @payload = "Hello, world!" queue.subscribe do |metadata, payload| metadata.routing_key.should == "amqpgem.key" metadata.content_type.should == "application/octet-stream" metadata.priority.should == 8 time = metadata.headers["time"] time.year.should == @now.year time.month.should == @now.month time.day.should == @now.day time.hour.should == @now.hour time.min.should == @now.min time.sec.should == @now.sec metadata.headers["coordinates"]["latitude"].should == 59.35 metadata.headers["participants"].should == 11 metadata.headers["venue"].should == "Stockholm" metadata.headers["true_field"].should == true metadata.headers["false_field"].should == false metadata.headers["nil_field"].should be_nil metadata.headers["ary_field"].should == ["one", 2.0, 3, [{ "abc" => 123 }]] metadata.timestamp.should == Time.at(@now.to_i) metadata.type.should == "kinda.checkin" metadata.consumer_tag.should_not be_nil metadata.consumer_tag.should_not be_empty metadata.delivery_tag.should == 1 metadata.reply_to.should == "a.sender" metadata.correlation_id.should == "r-1" metadata.message_id.should == "m-1" metadata.should_not be_redelivered metadata.app_id.should == "amqpgem.example" metadata.exchange.should == "amq.direct" payload.should == @payload done end exchange.publish(@payload, :app_id => "amqpgem.example", :priority => 8, :type => "kinda.checkin", # headers table keys can be anything :headers => { :coordinates => { :latitude => 59.35, :longitude => 18.066667 }, :time => @now, :participants => 11, :venue => "Stockholm", :true_field => true, :false_field => false, :nil_field => nil, :ary_field => ["one", 2.0, 3, [{ "abc" => 123 }]] }, :timestamp => @now.to_i, :reply_to => "a.sender", :correlation_id => "r-1", :message_id => "m-1", :routing_key => "amqpgem.key") end end amqp-1.8.0/spec/integration/redelivery_of_unacknowledged_messages_spec.rb0000644000004100000410000000505013321132064027114 0ustar www-datawww-data# encoding: utf-8 require 'spec_helper' describe "Unacknowledged messages" do # # Environment # include EventedSpec::AMQPSpec default_timeout 5 amqp_before do @connection1 = AMQP.connect @connection2 = AMQP.connect @connection3 = AMQP.connect @channel1 = AMQP::Channel.new(@connection1) @channel2 = AMQP::Channel.new(@connection2) @channel3 = AMQP::Channel.new(@connection3) [@channel1, @channel2, @channel3].each { |ch| ch.on_error { fail } } @channel1.prefetch(3) @channel2.prefetch(1) end after(:all) do AMQP.cleanup_state done end # # Examples # # this is a spec example based on one of the Working With Queues doc guides. # It is somewhat hairy since it imitates 3 apps in a single process # but demonstrates redeliveries pretty well. MK. it "are redelivered to alternate consumers when the 'primary' one disconnects" do number_of_messages_app2_received = 0 expected_number_of_deliveries = 21 redelivery_values = Array.new exchange = @channel3.direct("amq.direct") queue1 = @channel1.queue("amqpgem.examples.acknowledgements.explicit", :auto_delete => false) # purge the queue so that we don't get any redeliveries from previous runs queue1.purge queue1.bind(exchange).subscribe(:ack => true) do |metadata, payload| # acknowledge some messages, they will be removed from the queue if metadata.headers["i"] < 10 @channel1.acknowledge(metadata.delivery_tag, false) else # some messages are not ack-ed and will remain in the queue for redelivery # when app #1 connection is closed (either properly or due to a crash) end end queue2 = @channel2.queue!("amqpgem.examples.acknowledgements.explicit", :auto_delete => false) queue2.subscribe(:ack => true) do |metadata, payload| redelivery_values << metadata.redelivered? # app 2 always acks messages metadata.ack number_of_messages_app2_received += 1 end EventMachine.add_timer(2.0) { # app1 quits/crashes @connection1.close } # 0.5 seconds later, publish a bunch of messages EventMachine.add_timer(0.5) { 30.times do |i| exchange.publish("Message ##{i}", :headers => { :i => i }) i += 1 end } done(4.8) { number_of_messages_app2_received.should be >= expected_number_of_deliveries # 3 last messages are redeliveries redelivery_values.last(7).should == [false, false, false, false, true, true, true] } end end amqp-1.8.0/spec/integration/connection_level_exception_handling_spec.rb0000644000004100000410000000206513321132064026570 0ustar www-datawww-data# encoding: utf-8 require 'spec_helper' describe "Connection-level exception" do # # Environment # include EventedSpec::AMQPSpec default_timeout 2 amqp_before do @connection = AMQP.connect @channel = AMQP::Channel.new(@connection) end after(:all) do AMQP.cleanup_state done end # # Examples # it "can be handled with Session#on_error" do @connection.on_error do |conn, connection_close| conn.should == @connection connection_close.method_id.should == 31 connection_close.class_id.should == 10 connection_close.reply_code.should == 504 connection_close.reply_text.should_not be_nil connection_close.reply_text.should_not be_empty done end EventMachine.add_timer(0.3) do # send_frame is NOT part of the public API, but it is public for entities like AMQ::Client::Channel # and we use it here to trigger a connection-level exception. MK. @connection.send_frame(AMQ::Protocol::Connection::TuneOk.encode(1000, 1024 * 128 * 1024, 10)) end end end amqp-1.8.0/spec/integration/fanout_exchange_routing_spec.rb0000644000004100000410000000431213321132064024222 0ustar www-datawww-data# encoding: utf-8 require "spec_helper" describe AMQP::Exchange, "of type fanout" do # # Environment # include EventedSpec::AMQPSpec include EventedSpec::SpecHelper em_before { AMQP.cleanup_state } em_after { AMQP.cleanup_state } default_options AMQP_OPTS default_timeout 6 amqp_before do @channel = AMQP::Channel.new @channel.should be_open end after(:all) do AMQP.cleanup_state done end # # Examples # context "with three bound queues" do it "routes all messages to all bound queues" do @exchange = @channel.fanout("amqpgem.integration.multicast.fanout", :auto_delete => true) @queue1 = @channel.queue("amqpgem.integration.multicast.queue1", :auto_delete => true) @queue2 = @channel.queue("amqpgem.integration.multicast.queue2", :auto_delete => true) @queue3 = @channel.queue("amqpgem.integration.multicast.queue3", :auto_delete => true) @queues = [@queue1, @queue2, @queue3] @received_messages = { @queue1.name => [], @queue2.name => [], @queue3.name => [] } @expected_number_of_messages = { @queue1.name => 10, @queue2.name => 10, @queue3.name => 10 } @sent_values = Array.new @queue1.bind(@exchange).subscribe do |payload| @received_messages[@queue1.name].push(payload.to_i) end # subscribe @queue2.bind(@exchange).subscribe do |payload| @received_messages[@queue2.name].push(payload.to_i) end # subscribe @queue3.bind(@exchange).subscribe do |payload| @received_messages[@queue3.name].push(payload.to_i) end # subscribe 10.times do dispatched_data = rand(5_000_000) @sent_values.push(dispatched_data) @exchange.publish(dispatched_data, :mandatory => false) end # for Rubinius, it is surprisingly slow on this workload done(1.5) { [@queue1, @queue2, @queue3].each do |q| @received_messages[q.name].size.should == @expected_number_of_messages[q.name] # this one is ordering assertion @received_messages[q.name].should == @sent_values end } end # it end end # describe amqp-1.8.0/spec/integration/remove_individual_binding_spec.rb0000644000004100000410000000207413321132064024517 0ustar www-datawww-data# encoding: utf-8 require 'spec_helper' describe "An individual binding" do # # Environment # include EventedSpec::AMQPSpec default_timeout 3 amqp_before do @channel = AMQP::Channel.new @channel.should be_open @channel.on_error do |ch, close| raise "Channel-level error!: #{close.inspect}" end end default_options AMQP_OPTS it "can be deleted by specifying routing key" do flag1 = false flag2 = false x = @channel.direct("amqpgem.examples.imaging") q = @channel.queue("", :exclusive => true) q.bind(x, :routing_key => "resize").bind(x, :routing_key => "watermark").subscribe do |meta, payload| flag1 = (meta.routing_key == "watermark") flag2 = (meta.routing_key == "resize") end EventMachine.add_timer(0.5) do q.unbind(x, :routing_key => "resize") do x.publish("some data", :routing_key => "resize") x.publish("some data", :routing_key => "watermark") end done(1.0) { flag1.should be_true flag2.should be_false } end end end amqp-1.8.0/spec/integration/stress/0000755000004100000410000000000013321132064017301 5ustar www-datawww-dataamqp-1.8.0/spec/integration/stress/publishing_of_messages_with_incrementing_sizes_spec.rb0000644000004100000410000000175713321132064032403 0ustar www-datawww-data# encoding: utf-8 require "spec_helper" # this example group ensures no message size is special # (with respect to framing edge cases). MK. describe "Stress test with messages with incrementing sizes" do # # Environment # include EventedSpec::AMQPSpec default_options AMQP_OPTS default_timeout 60 amqp_before do @channel = AMQP::Channel.new @channel.on_error do |ch, close| raise "Channel-level error!: #{close.inspect}" end @queue = @channel.queue("", :auto_delete => true) @exchange = @channel.fanout("amqpgem.tests.integration.fanout.exchange", :auto_delete => true) @queue.bind(@exchange) end # # Examples # it "passes" do list = Range.new(0, 4785).to_a received = Array.new @queue.subscribe do |metadata, payload| received << payload.bytesize done if received == list end EventMachine.add_timer(1.0) do list.each do |n| @exchange.publish("i" * n) end end end endamqp-1.8.0/spec/integration/hello_world_spec.rb0000644000004100000410000000322213321132064021626 0ustar www-datawww-data# encoding: utf-8 require "spec_helper" describe "A 'Hello, world'-like example with a non-empty message" do # # Environment # include EventedSpec::AMQPSpec default_options AMQP_OPTS default_timeout 5 amqp_before do @channel = AMQP::Channel.new @channel.on_error do |ch, close| raise "Channel-level error!: #{close.inspect}" end @queue = @channel.queue("", :auto_delete => true) @exchange = @channel.direct("amqpgem.tests.integration.direct.exchange", :auto_delete => true) @queue.bind(@exchange, :routing_key => "messages.all") end it "is delivered" do consumer1 = AMQP::Consumer.new(@channel, @queue).consume consumer1.on_delivery do |metadata, payload| done end EventMachine.add_timer(0.5) do @exchange.publish("Hello!", :routing_key => "messages.all") end end end describe "A 'Hello, world'-like example with a blank message" do # # Environment # include EventedSpec::AMQPSpec default_options AMQP_OPTS default_timeout 5 amqp_before do @channel = AMQP::Channel.new @channel.on_error do |ch, close| raise "Channel-level error!: #{close.inspect}" end @queue = @channel.queue("", :auto_delete => true) @exchange = @channel.direct("amqpgem.tests.integration.direct.exchange", :auto_delete => true) @queue.bind(@exchange, :routing_key => "messages.all") end it "is delivered" do consumer1 = AMQP::Consumer.new(@channel, @queue).consume consumer1.on_delivery do |metadata, payload| done end EventMachine.add_timer(0.5) do @exchange.publish("", :routing_key => "messages.all") end end endamqp-1.8.0/spec/integration/declare_one_hundred_server_named_queues_spec.rb0000644000004100000410000000131113321132064027403 0ustar www-datawww-data# encoding: utf-8 require 'spec_helper' describe "Server-named", AMQP::Queue do # # Environment # include EventedSpec::AMQPSpec default_timeout 5 amqp_before do @channel = AMQP::Channel.new end # # Examples # it "can be declared en masse" do n = 100 queues = [] n.times do queues << @channel.queue("", :auto_delete => true) end done(2.5) { queues.size.should == n # this is RabbitMQ-specific. But it is OK for now. MK. queues.all? { |q| q.name =~ /^amq.*/ }.should be_true # no duplicates. MK. names = queues.map { |q| q.name } names.uniq.size.should == n names.uniq.should == names } end end amqp-1.8.0/spec/integration/authentication_spec.rb0000644000004100000410000000742113321132064022340 0ustar www-datawww-data# encoding: utf-8 require "spec_helper" describe "Authentication attempt" do # # Environment # include EventedSpec::EMSpec include EventedSpec::SpecHelper default_timeout 7.0 describe "with default connection parameters" do # # Examples # # assuming there is an account guest with password of "guest" that has # access to / (default vhost) context "when guest/guest has access to /" do after :all do done end it "succeeds" do AMQP.connect do |connection| connection.should be_open connection.close { done } end end # it end # context end # describe describe "with explicitly given connection parameters" do # # Examples # # assuming there is an account amqp_gem with password of "amqp_gem_password" that has # access to amqp_gem_testbed context "when amqp_gem/amqp_gem_testbed has access to amqp_gem_testbed" do context "and provided credentials are correct" do it "succeeds" do connection = AMQP.connect(AMQP_OPTS.merge(:username => "amqp_gem", :password => "amqp_gem_password", :vhost => "amqp_gem_testbed")) done(3.0) { connection.should be_connected connection.username.should == "amqp_gem" connection.user.should == "amqp_gem" connection.hostname.should == "localhost" connection.host.should == "localhost" connection.port.should == 5672 connection.vhost.should == "amqp_gem_testbed" connection.broker_endpoint.should == "localhost:5672/amqp_gem_testbed" connection.close } end # it end # context context "and provided credentials ARE INCORRECT" do default_timeout 5 after(:all) { done } it "fails" do handler = Proc.new { |settings| puts "Callback has fired" callback_has_fired = true done } connection = AMQP.connect(:username => "amqp_gem", :password => Time.now.to_i.to_s, :vhost => "amqp_gem_testbed", :on_possible_authentication_failure => handler) end # it end context "and provided vhost DOES NOT EXIST" do default_timeout 10 after(:all) { done } it "fails" do handler = Proc.new { |settings| puts "Callback has fired" callback_has_fired = true done } connection = AMQP.connect(:username => "amqp_gem", :password => Time.now.to_i.to_s, :vhost => "/a/b/c/#{rand}/#{Time.now.to_i}", :on_possible_authentication_failure => handler) end # it end end # context end describe "with connection string" do # # Examples # # assuming there is an account amqp_gem with password of "amqp_gem_password" that has # access to amqp_gem_testbed context "when amqp_gem/amqp_gem_testbed has access to amqp_gem_testbed" do context "and provided credentials are correct" do it "succeeds" do connection = AMQP.connect "amqp://amqp_gem:amqp_gem_password@localhost/amqp_gem_testbed" done(3.0) { connection.should be_connected connection.close } end # it end # context context "and provided credentials ARE INCORRECT" do default_timeout 6 after(:all) { done } it "fails" do connection = AMQP.connect "amqp://amqp_gem:#{Time.now.to_i}@localhost/amqp_gem_testbed", :on_possible_authentication_failure => Proc.new { |settings| puts "Callback has fired" done } end # it end # context end # context end # describe end # describe "Authentication attempt" amqp-1.8.0/spec/integration/store_and_forward_spec.rb0000644000004100000410000000447213321132064023026 0ustar www-datawww-data# encoding: utf-8 require "spec_helper" describe "Store-and-forward routing" do # # Environment # include EventedSpec::AMQPSpec include EventedSpec::SpecHelper em_before { AMQP.cleanup_state } em_after { AMQP.cleanup_state } default_options AMQP_OPTS default_timeout 10 amqp_before do @channel = AMQP::Channel.new @channel.should be_open end after(:all) do AMQP.cleanup_state done end # # Examples # context "that uses fanout exchange" do context "with a single bound queue" do amqp_before do @queue_name = "amqpgem.integration.snf.queue1" @exchange = @channel.direct("") @queue = @channel.queue(@queue_name, :auto_delete => true, :nowait => false) end it "allows asynchronous subscription to messages WITHOUT acknowledgements" do number_of_received_messages = 0 # put a little pressure expected_number_of_messages = 300 # It is always a good idea to use non-ASCII charachters in # various test suites. MK. dispatched_data = "messages sent at #{Time.now.to_i}" @queue.purge @queue.subscribe(:ack => false) do |payload| payload.should be_instance_of(String) number_of_received_messages += 1 payload.should == dispatched_data end # subscribe delayed(0.3) do expected_number_of_messages.times do @exchange.publish(dispatched_data, :routing_key => @queue_name) end end done(4.0) { number_of_received_messages.should == expected_number_of_messages @queue.unsubscribe } end # it it "allows asynchronous subscription to messages WITH acknowledgements" do number_of_received_messages = 0 expected_number_of_messages = 500 @queue.subscribe(:ack => true) do |payload| number_of_received_messages += 1 end # subscribe expected_number_of_messages.times do @exchange.publish(rand, :key => @queue_name) end # 5 seconds are for Rubinius, it is surprisingly slow on this workload done(5.0) { number_of_received_messages.should == expected_number_of_messages @queue.unsubscribe } end # it end end # context end # describe amqp-1.8.0/spec/integration/declare_and_immediately_bind_a_server_named_queue_spec.rb0000644000004100000410000000242413321132064031355 0ustar www-datawww-data# encoding: utf-8 require 'spec_helper' describe "Server-named", AMQP::Queue do # # Environment # include EventedSpec::AMQPSpec default_timeout 2 # # Examples # context "bound to a pre-defined exchange" do it "delays binding until after queue.declare-ok arrives" do mailbox = [] channel = AMQP::Channel.new exchange = channel.fanout("amq.fanout") input = "Independencia de resolución, ¿una realidad en Mac OS X Lion?" channel.queue("", :auto_delete => true).bind(exchange).subscribe do |header, body| mailbox << body end delayed(0.5) { exchange.publish(input) } done(1.0) { mailbox.size.should == 1 } end end context "bound to a pre-defined exchange" do it "delays binding until after queue.declare-ok arrives" do mailbox = [] channel = AMQP::Channel.new exchange = channel.fanout("amqpgem.examples.exchanges.fanout", :durable => false) input = "Just send me already" channel.queue("", :auto_delete => true).bind(exchange).subscribe do |header, body| mailbox << body end delayed(0.5) { exchange.publish(input) } done(1.0) { mailbox.size.should == 1 } end end end amqp-1.8.0/spec/integration/multiple_consumers_per_queue_spec.rb0000644000004100000410000001107013321132064025317 0ustar www-datawww-data# encoding: utf-8 require "spec_helper" describe "Multiple non-exclusive consumers per queue" do # # Environment # include EventedSpec::AMQPSpec include EventedSpec::SpecHelper default_options AMQP_OPTS default_timeout 10 let(:messages) { (0..99).map {|i| "Message #{i}" } } # # Examples # before :each do @consumer1_mailbox = [] @consumer2_mailbox = [] @consumer3_mailbox = [] end context "with equal prefetch levels" do it "have messages distributed to them in the round-robin manner" do channel = AMQP::Channel.new channel.on_error do |ch, channel_close| raise(channel_close.reply_text) end queue = channel.queue("amqpgem.integration.roundrobin.queue1", :auto_delete => true) do consumer1 = AMQP::Consumer.new(channel, queue) consumer2 = AMQP::Consumer.new(channel, queue, "#{queue.name}-consumer-#{rand}-#{Time.now}", false, true) consumer1.consume.on_delivery do |basic_deliver, metadata, payload| @consumer1_mailbox << payload end consumer2.consume(true).on_delivery do |metadata, payload| @consumer2_mailbox << payload end queue.subscribe do |metadata, payload| @consumer3_mailbox << payload end end exchange = channel.default_exchange exchange.on_return do |basic_return, metadata, payload| raise(basic_return.reply_text) end EventMachine.add_timer(1.0) do messages.each do |message| exchange.publish(message, :mandatory => true, :routing_key => queue.name) end end done(6.5) { @consumer1_mailbox.size.should == 34 @consumer2_mailbox.size.should == 33 @consumer3_mailbox.size.should == 33 } end # it end # context context "with equal prefetch levels and when queue is server-named" do it "have messages distributed to them in the round-robin manner" do channel = AMQP::Channel.new channel.on_error do |ch, channel_close| raise(channel_close.reply_text) end queue = channel.queue("amqpgem.integration.roundrobin.queue1", :auto_delete => true) do consumer1 = AMQP::Consumer.new(channel, queue) consumer2 = AMQP::Consumer.new(channel, queue, "#{queue.name}-consumer-#{rand}-#{Time.now}", false, true) consumer1.consume.on_delivery do |basic_deliver, metadata, payload| @consumer1_mailbox << payload end consumer2.consume(true).on_delivery do |metadata, payload| @consumer2_mailbox << payload end queue.subscribe do |metadata, payload| @consumer3_mailbox << payload end queue.consumer_tag.should == queue.default_consumer.consumer_tag end exchange = channel.default_exchange exchange.on_return do |basic_return, metadata, payload| raise(basic_return.reply_text) end EventMachine.add_timer(1.0) do messages.each do |message| exchange.publish(message, :mandatory => true, :routing_key => queue.name) end end done(5.0) { @consumer1_mailbox.size.should == 34 @consumer2_mailbox.size.should == 33 @consumer3_mailbox.size.should == 33 } end # it end # context context "with equal prefetch levels and one consumer cancelled mid-flight" do it "have messages distributed to them in the round-robin manner" do channel = AMQP::Channel.new channel.on_error do |ch, channel_close| raise(channel_close.reply_text) end queue = channel.queue("", :auto_delete => true) consumer1 = AMQP::Consumer.new(channel, queue) consumer2 = AMQP::Consumer.new(channel, queue) consumer1.consume.on_delivery do |basic_deliver, metadata, payload| @consumer1_mailbox << payload end consumer2.consume(true).on_delivery do |metadata, payload| @consumer2_mailbox << payload end queue.subscribe do |metadata, payload| @consumer3_mailbox << payload end consumer2.cancel exchange = channel.default_exchange exchange.on_return do |basic_return, metadata, payload| raise(basic_return.reply_text) end EventMachine.add_timer(1.0) do messages.each do |message| exchange.publish(message, :mandatory => true, :routing_key => queue.name) end end done(5.0) { @consumer1_mailbox.size.should == 50 @consumer2_mailbox.size.should == 0 @consumer3_mailbox.size.should == 50 } end # it end # context end # describe amqp-1.8.0/spec/integration/recovery/0000755000004100000410000000000013321132064017614 5ustar www-datawww-dataamqp-1.8.0/spec/integration/recovery/per_channel_automatic_recovery_spec.rb0000644000004100000410000000342613321132064027422 0ustar www-datawww-data# encoding: utf-8 require 'spec_helper' unless ENV["CI"] describe "Per-channel automatic channel recovery" do # # Environment # include RabbitMQ::Control include EventedSpec::AMQPSpec default_timeout 20.0 amqp_before do @channel = AMQP::Channel.new(AMQP.connection, :auto_recovery => true) end after :all do start_rabbitmq unless rabbitmq_pid end # ... # # Examples # it "kicks in when broker goes down" do AMQP.connection.on_tcp_connection_loss do |conn, settings| puts "[network failure] Trying to reconnect..." conn.reconnect(false, 1) end pid = rabbitmq_pid puts "rabbitmq pid = #{pid.inspect}" kill_rabbitmq rabbitmq_pid.should be_nil # 2 seconds later, check that we are reconnecting EventMachine.add_timer(2.0) do AMQP.connection.should_not be_connected AMQP.connection.should be_reconnecting end # 4 seconds later, start RabbitMQ EventMachine.add_timer(4.0) do start_rabbitmq rabbitmq_pid.should_not be_nil end # 12 seconds later, use the (now recovered) connection. Note that depending # on # of plugins used it may take 5-10 seconds to start up RabbitMQ and initialize it, # then open a new AMQP connection. That's why we wait. MK. EventMachine.add_timer(12.0) do AMQP.connection.should be_connected AMQP.connection.should_not be_reconnecting @channel.queue("amqpgem.tests.a.queue", :auto_delete => true).subscribe do |metadata, payload| puts "Got a message" done end EventMachine.add_timer(1.5) { @channel.default_exchange.publish("Hi", :routing_key => "amqpgem.tests.a.queue") } end end end end././@LongLink0000644000000000000000000000015000000000000011577 Lustar rootrootamqp-1.8.0/spec/integration/recovery/per_channel_automatic_recovery_on_graceful_broker_shutdown_spec.rbamqp-1.8.0/spec/integration/recovery/per_channel_automatic_recovery_on_graceful_broker_shutdown_spec0000644000004100000410000000401213321132064034653 0ustar www-datawww-data# encoding: utf-8 require 'spec_helper' unless ENV["CI"] describe "Per-channel automatic channel recovery" do # # Environment # include RabbitMQ::Control include EventedSpec::AMQPSpec default_timeout 20.0 amqp_before do @channel = AMQP::Channel.new(AMQP.connection, :auto_recovery => true) end after :all do start_rabbitmq unless rabbitmq_pid end # ... # # Examples # it "can be used when broker is shut down gracefully" do AMQP.connection.on_error do |conn, connection_close| puts "[connection.close] Reply code = #{connection_close.reply_code}, reply text = #{connection_close.reply_text}" if connection_close.reply_code == 320 puts "[connection.close] Setting up a periodic reconnection timer..." conn.periodically_reconnect(1) end end pid = rabbitmq_pid puts "rabbitmq pid = #{pid.inspect}" stop_rabbitmq rabbitmq_pid.should be_nil # 2 seconds later, check that we are reconnecting EventMachine.add_timer(2.0) do AMQP.connection.should_not be_connected AMQP.connection.should be_reconnecting end # 4 seconds later, start RabbitMQ EventMachine.add_timer(4.0) do start_rabbitmq rabbitmq_pid.should_not be_nil end # 10 seconds later, use the (now recovered) connection. Note that depending # on # of plugins used it may take 5-10 seconds to start up RabbitMQ and initialize it, # then open a new AMQP connection. That's why we wait. MK. EventMachine.add_timer(10.0) do AMQP.connection.should be_connected AMQP.connection.should_not be_reconnecting @channel.queue("amqpgem.tests.a.queue", :auto_delete => true).subscribe do |metadata, payload| puts "Got a message: #{payload.inspect}" done end EventMachine.add_timer(1.5) { @channel.default_exchange.publish("", :routing_key => "amqpgem.tests.a.queue") } end end end end amqp-1.8.0/spec/integration/message_acknowledgement_spec.rb0000644000004100000410000000174613321132064024200 0ustar www-datawww-data# encoding: utf-8 require 'spec_helper' unless ENV["CI"] describe "Message acknowledgements" do # # Environment # include EventedSpec::AMQPSpec default_timeout 120 amqp_before do @connection = AMQP.connect @channel1 = AMQP::Channel.new(@connection) @channel2 = AMQP::Channel.new(@connection) end it "can be issued for delivery tags >= 192" do exchange_name = "amqpgem.tests.fanout#{rand}" queue = @channel1.queue("", :auto_delete => true).bind(exchange_name).subscribe(:ack => true) do |metadata, payload| puts "x-sequence = #{metadata.headers['x-sequence']}, delivery_tag = #{metadata.delivery_tag}" if ENV["DEBUG"] metadata.ack if metadata.delivery_tag >= 999 done(1.0) end end exchange = @channel2.fanout(exchange_name, :durable => false) 2000.times do |i| exchange.publish("", :headers => { 'x-sequence' => i }) end end end endamqp-1.8.0/spec/integration/reply_queue_communication_spec.rb0000644000004100000410000000336013321132064024603 0ustar www-datawww-data# encoding: utf-8 require "spec_helper" describe "Exclusive server-named queue" do # # Environment # include EventedSpec::AMQPSpec default_timeout 2 amqp_before do @channel = AMQP::Channel.new @channel.should be_open @exchange = AMQP::Exchange.default(@channel) end it "can be used for temporary point-to-point communication" do @exchange.channel.should == @channel @channel.queue("", :exclusive => true) do |queue1| puts "First callback has fired" @channel.queue("", :exclusive => true) do |queue2| puts "Second callback has fired" request_timestamp = Time.now.to_i reply_timestamp = nil queue1.subscribe do |header, body| header.timestamp.to_i.should == request_timestamp.to_i header.app_id.should == "Client" header.reply_to.should == queue2.name reply_timestamp = Time.now.to_i @exchange.publish(rand(1000), :routing_key => header.reply_to, :reply_to => queue1.name, :app_id => "Server", :timestamp => reply_timestamp) end queue2.subscribe do |header, body| header.timestamp.to_i.should == reply_timestamp.to_i header.app_id.should == "Server" header.reply_to.should == queue1.name end # publish the request @exchange.publish(rand(1000), :routing_key => queue1.name, :reply_to => queue2.name, :app_id => "Client", :timestamp => request_timestamp, :mandatory => true) done(0.2) { queue1.unsubscribe queue2.unsubscribe } end # do end # do end # it end # describe amqp-1.8.0/spec/integration/tx_rollback_spec.rb0000644000004100000410000000632213321132064021624 0ustar www-datawww-data# encoding: utf-8 require 'spec_helper' describe "AMQP transaction rollback" do # # Environment # include EventedSpec::AMQPSpec default_timeout 4.5 amqp_before do @producer_channel = AMQP::Channel.new @consumer_channel = AMQP::Channel.new end # ... # # Examples # it "voids messages published since the last tx.select" do exchange = @producer_channel.fanout("amq.fanout") queue = @consumer_channel.queue("", :exclusive => true) queue.bind(exchange).subscribe do |metadata, payload| fail "Consumer received a message before transaction committed" end @producer_channel.tx_select EventMachine.add_timer(0.5) do 50.times { exchange.publish("before tx.commit") } @producer_channel.tx_rollback end done(3.5) end # it end # describe describe "AMQP connection closure that follows tx.select" do # # Environment # include EventedSpec::AMQPSpec default_timeout 4.5 amqp_before do @producer_channel = AMQP::Channel.new @consumer_channel = AMQP::Channel.new end # ... # # Examples # it "voids messages published since the last tx.select" do exchange = @producer_channel.fanout("amq.fanout") queue = @consumer_channel.queue("", :exclusive => true) queue.bind(exchange).subscribe do |metadata, payload| fail "Consumer received a message before transaction committed" end @producer_channel.tx_select EventMachine.add_timer(0.5) do 3.times { exchange.publish("before tx.commit") } @producer_channel.connection.close end done(3.5) end # it end # describe describe "AMQP channel closure that follows tx.select" do # # Environment # include EventedSpec::AMQPSpec default_timeout 4.5 amqp_before do @producer_channel = AMQP::Channel.new @consumer_channel = AMQP::Channel.new end # ... # # Examples # it "voids messages published since the last tx.select" do exchange = @producer_channel.fanout("amq.fanout") queue = @consumer_channel.queue("", :exclusive => true) queue.bind(exchange).subscribe do |metadata, payload| fail "Consumer received a message before transaction committed" end @producer_channel.tx_select EventMachine.add_timer(0.5) do 3.times { exchange.publish("before tx.commit") } @producer_channel.close end done(3.5) end # it end # describe describe "AMQP transaction rollback attempt on a non-transactional channel" do # # Environment # include EventedSpec::AMQPSpec default_timeout 4.5 amqp_before do @producer_channel = AMQP::Channel.new @consumer_channel = AMQP::Channel.new end # ... # # Examples # it "causes channel-level exception" do exchange = @producer_channel.fanout("amq.fanout") queue = @consumer_channel.queue("", :exclusive => true) queue.bind(exchange).subscribe do |metadata, payload| fail "Consumer received a message before transaction committed" end @producer_channel.on_error do |ch, channel_close| puts "#{channel_close.reply_text}" done end EventMachine.add_timer(0.5) { @producer_channel.tx_rollback } done(3.5) end # it end # describe amqp-1.8.0/spec/integration/tx_commit_spec.rb0000644000004100000410000000456413321132064021331 0ustar www-datawww-data# encoding: utf-8 require 'spec_helper' describe "Messages published before AMQP transaction commits" do # # Environment # include EventedSpec::AMQPSpec default_timeout 1.5 amqp_before do @producer_channel = AMQP::Channel.new @consumer_channel = AMQP::Channel.new end # ... # # Examples # it "are not accessible to AMQP consumers" do exchange = @producer_channel.fanout("amq.fanout") queue = @consumer_channel.queue("", :exclusive => true) queue.bind(exchange).subscribe do |metadata, payload| fail "Consumer received a message before transaction committed" end @producer_channel.tx_select EventMachine.add_timer(0.5) do 50.times { exchange.publish("before tx.commit") } # @producer_channel.tx_commit end done(1.2) end # it end # describe describe "AMQP transaction commit" do # # Environment # include EventedSpec::AMQPSpec default_timeout 1.5 amqp_before do @producer_channel = AMQP::Channel.new @consumer_channel = AMQP::Channel.new end # ... # # Examples # it "causes messages published since the last tx.select to be delivered to AMQP consumers" do exchange = @producer_channel.fanout("amq.fanout") queue = @consumer_channel.queue("", :exclusive => true) queue.bind(exchange).subscribe { |metadata, payload| done } @producer_channel.tx_select EventMachine.add_timer(0.5) do 50.times { exchange.publish("before tx.commit") } @producer_channel.tx_commit end done(1.2) end # it end # describe describe "AMQP transaction commit attempt on a non-transactional channel" do # # Environment # include EventedSpec::AMQPSpec default_timeout 1.5 amqp_before do @producer_channel = AMQP::Channel.new @consumer_channel = AMQP::Channel.new end # ... # # Examples # it "causes channel-level exception" do exchange = @producer_channel.fanout("amq.fanout") queue = @consumer_channel.queue("", :exclusive => true) queue.bind(exchange).subscribe do |metadata, payload| fail "Consumer received a message before transaction committed" end @producer_channel.on_error do |ch, channel_close| puts "#{channel_close.reply_text}" done end EventMachine.add_timer(0.5) { @producer_channel.tx_commit } done(1.2) end # it end # describe amqp-1.8.0/spec/integration/concurrent_publishing_on_a_shared_channel_spec.rb0000644000004100000410000000710213321132064027735 0ustar www-datawww-data# encoding: utf-8 require 'spec_helper' require "multi_json" describe "Concurrent publishing on a shared channel" do # # Environment # include EventedSpec::AMQPSpec default_timeout 10 amqp_before do @channel = AMQP::Channel.new @channel.should be_open @channel.on_error do |ch, close| raise "Channel-level error!: #{close.inspect}" end @queue1 = @channel.queue("amqpgem.tests.integration.queue#{Time.now.to_i}#{rand}", :exclusive => true) @queue2 = @channel.queue("amqpgem.tests.integration.queue#{Time.now.to_i}#{rand}", :exclusive => true) # Rely on default direct exchange binding, see section 2.1.2.4 Automatic Mode in AMQP 0.9.1 spec. @exchange = AMQP::Exchange.default(@channel) end default_options AMQP_OPTS let(:inputs) do [ { :index=>{:_routing=>530,:_index=>"optimizer",:_type=>"earnings",:_id=>530}}, { :total_conversions=>0,:banked_clicks=>0,:total_earnings=>0,:pending_conversions=>0,:paid_net_earnings=>0,:banked_conversions=>0,:pending_earnings=>0,:optimizer_id=>530,:total_impressions=>0,:banked_earnings=>0,:bounce_count=>0,:time_on_page=>0,:total_clicks=>0,:entrances=>0,:pending_clicks=>0,:paid_earnings=>0}, { :index=>{:_routing=>430,:_index=>"optimizer",:_type=>"earnings",:_id=>430}}, { :total_conversions=>1443,:banked_clicks=>882,:total_earnings=>5796.3315841537,:pending_conversions=>22,:paid_net_earnings=>4116.90224486802,:banked_conversions=>1086,:pending_earnings=>257.502767857143,:optimizer_id=>430,:total_impressions=>6370497,:banked_earnings=>122.139339285714,:bounce_count=>6825,:time_on_page=>0,:total_clicks=>38143,:entrances=>12336,:pending_clicks=>1528,:paid_earnings=>5670.78224486798}, { :index=>{:_routing=>506,:_index=>"optimizer",:_type=>"earnings",:_id=>506}}, { :total_conversions=>237,:banked_clicks=>232,:total_earnings=>550.6212071428588277,:pending_conversions=>9,:paid_net_earnings=>388.021207142857,:banked_conversions=>225,:pending_earnings=>150.91,:optimizer_id=>506,:total_impressions=>348319,:banked_earnings=>12.92,:bounce_count=>905,:time_on_page=>0,:total_clicks=>4854,:entrances=>1614,:pending_clicks=>1034,:paid_earnings=>537.501207142858}, {:index=>{:_routing=>345,:_index=>"optimizer",:_type=>"earnings",:_id=>345}}, {:total_conversions=>0,:banked_clicks=>0,:total_earnings=>0,:pending_conversions=>0,:paid_net_earnings=>0,:banked_conversions=>0,:pending_earnings=>0,:optimizer_id=>345,:total_impressions=>0,:banked_earnings=>0,:bounce_count=>0,:time_on_page=>0,:total_clicks=>0,:entrances=>0,:pending_clicks=>0,:paid_earnings=>0} ] end let(:messages) { inputs.map {|i| MultiJson.encode(i) } * 3 } it "frames bodies correctly" do @exchange.channel.should == @channel number_of_received_messages = 0 expected_number_of_messages = 3 dispatched_data = "to be received by queue1" @queue1.subscribe do |payload| number_of_received_messages += 1 payload.should == dispatched_data end # subscribe 4.times do Thread.new do @exchange.publish(body, :routing_key => "killa key") end end expected_number_of_messages.times do Thread.new do @exchange.publish(dispatched_data, :routing_key => @queue1.name) end end 4.times do Thread.new do @exchange.publish(body, :routing_key => "killa key") end end delayed(0.6) { # We never subscribe to it, hence, need to delete manually @queue2.delete } done(5.0) { number_of_received_messages.should == expected_number_of_messages } end end # describe amqp-1.8.0/spec/integration/topic_subscription_spec.rb0000644000004100000410000001455513321132064023251 0ustar www-datawww-data# encoding: utf-8 require "spec_helper" describe "Topic-based subscription" do # # Environment # include EventedSpec::AMQPSpec include EventedSpec::SpecHelper em_before { AMQP.cleanup_state } em_after { AMQP.cleanup_state } default_options AMQP_OPTS default_timeout 10 amqp_before do @channel = AMQP::Channel.new @channel.should be_open @exchange = @channel.topic end after(:all) do AMQP.cleanup_state done end # # Examples # context "when keys match exactly" do it "routes messages to bound queues" do @aapl_queue = @channel.queue("AAPL queue", :auto_delete => true) @amzn_queue = @channel.queue("AMZN queue", :auto_delete => true) received_messages = { @aapl_queue.name => 0, @amzn_queue.name => 0 } expected_messages = { @aapl_queue.name => 4, @amzn_queue.name => 2 } @aapl_queue.bind(@exchange, :key => "nasdaq.aapl").subscribe do |payload| received_messages[@aapl_queue.name] += 1 end @amzn_queue.bind(@exchange, :key => "nasdaq.amzn").subscribe do |payload| received_messages[@amzn_queue.name] += 1 end 4.times do @exchange.publish(332 + rand(1000)/400.0, :key => "nasdaq.aapl") end 2.times do @exchange.publish(181 + rand(1000)/400.0, :key => "nasdaq.amzn") end # publish some messages none of our queues should be receiving 3.times do @exchange.publish(626 + rand(1000)/400.0, :key => "nasdaq.goog") end done(0.5) { received_messages.should == expected_messages @aapl_queue.unsubscribe @amzn_queue.unsubscribe } end # it end # context context "when key matches on * (single word globbing)" do it "routes messages to bound queues" do @nba_queue = @channel.queue("NBA queue", :auto_delete => true) @knicks_queue = @channel.queue("New York Knicks queue", :auto_delete => true) @celtics_queue = @channel.queue("Boston Celtics queue", :auto_delete => true) received_messages = { @nba_queue.name => 0, @knicks_queue.name => 0, @celtics_queue.name => 0 } expected_messages = { @nba_queue.name => 7, @knicks_queue.name => 2, @celtics_queue.name => 3 } @nba_queue.bind(@exchange, :key => "sports.nba.*").subscribe do |payload| received_messages[@nba_queue.name] += 1 end @knicks_queue.bind(@exchange, :key => "sports.nba.knicks").subscribe do |payload| received_messages[@knicks_queue.name] += 1 end @celtics_queue.bind(@exchange, :key => "sports.nba.celtics").subscribe do |payload| received_messages[@celtics_queue.name] += 1 end @exchange.publish("Houston Rockets 104 : New York Knicks 89", :key => "sports.nba.knicks") @exchange.publish("Phoenix Suns 129 : New York Knicks 121", :key => "sports.nba.knicks") @exchange.publish("Ray Allen hit a 21-foot jumper with 24.5 seconds remaining on the clock to give Boston a win over Detroit last night in the TD Garden", :key => "sports.nba.celtics") @exchange.publish("Garnett's Return Sparks Celtics Over Magic at Garden", :key => "sports.nba.celtics") @exchange.publish("Tricks of the Trade: Allen Reveals Magic of Big Shots", :key => "sports.nba.celtics") @exchange.publish("Blatche, Wall lead Wizards over Jazz 108-101", :key => "sports.nba.jazz") @exchange.publish("Deron Williams Receives NBA Cares Community Assist Award", :key => "sports.nba.jazz") done(0.6) { received_messages.should == expected_messages @nba_queue.unsubscribe @knicks_queue.unsubscribe @celtics_queue.unsubscribe } end # it end # context context "when key matches on # (multiple words globbing)" do it "routes messages to bound queues" do @sports_queue = @channel.queue("Sports queue", :auto_delete => true) @nba_queue = @channel.queue("NBA queue", :auto_delete => true) @knicks_queue = @channel.queue("New York Knicks queue", :auto_delete => true) @celtics_queue = @channel.queue("Boston Celtics queue", :auto_delete => true) received_messages = { @sports_queue.name => 0, @nba_queue.name => 0, @knicks_queue.name => 0, @celtics_queue.name => 0 } expected_messages = { @sports_queue.name => 9, @nba_queue.name => 7, @knicks_queue.name => 2, @celtics_queue.name => 3 } @sports_queue.bind(@exchange, :key => "sports.#").subscribe do |payload| received_messages[@sports_queue.name] += 1 end @nba_queue.bind(@exchange, :key => "*.nba.*").subscribe do |payload| received_messages[@nba_queue.name] += 1 end @knicks_queue.bind(@exchange, :key => "sports.nba.knicks").subscribe do |payload| received_messages[@knicks_queue.name] += 1 end @celtics_queue.bind(@exchange, :key => "#.celtics").subscribe do |payload| received_messages[@celtics_queue.name] += 1 end @exchange.publish("Houston Rockets 104 : New York Knicks 89", :key => "sports.nba.knicks") @exchange.publish("Phoenix Suns 129 : New York Knicks 121", :key => "sports.nba.knicks") @exchange.publish("Ray Allen hit a 21-foot jumper with 24.5 seconds remaining on the clock to give Boston a win over Detroit last night in the TD Garden", :key => "sports.nba.celtics") @exchange.publish("Garnett's Return Sparks Celtics Over Magic at Garden", :key => "sports.nba.celtics") @exchange.publish("Tricks of the Trade: Allen Reveals Magic of Big Shots", :key => "sports.nba.celtics") @exchange.publish("Blatche, Wall lead Wizards over Jazz 108-101", :key => "sports.nba.jazz") @exchange.publish("Deron Williams Receives NBA Cares Community Assist Award", :key => "sports.nba.jazz") @exchange.publish("Philadelphia's Daniel Briere has been named as an All-Star replacement for Jarome Iginla.", :key => "sports.nhl.allstargame") @exchange.publish("Devils blank Sid- and Malkin-less Penguins 2-0", :key => "sports.nhl.penguins") done(0.5) { received_messages.should == expected_messages @sports_queue.unsubscribe @nba_queue.unsubscribe @knicks_queue.unsubscribe @celtics_queue.unsubscribe } end # it end # context end # describe amqp-1.8.0/spec/integration/headers_exchange_routing_spec.rb0000644000004100000410000002117713321132064024351 0ustar www-datawww-data# encoding: utf-8 require 'spec_helper' describe "Headers exchange" do # # Environment # include EventedSpec::AMQPSpec default_timeout 5 amqp_before do @connection = AMQP.connect @channel = AMQP::Channel.new(@connection) @channel.on_error do |ch, channel_close| fail "A channel-level exception: #{channel_close.inspect}" end end after(:all) do AMQP.cleanup_state done end # # Examples # # it would be following good practices to split this into # 2 separate examples but I think this particular example # is complete because it demonstrates routing in cases when # different queues are bound with x-match = any AND x-match = all. MK. it "can route messages based on any or all of N headers" do exchange = @channel.headers("amq.match", :durable => true) linux_and_ia64_messages = [] @channel.queue("", :auto_delete => true).bind(exchange, :arguments => { 'x-match' => 'all', :arch => "IA64", :os => 'linux' }).subscribe do |metadata, payload| linux_and_ia64_messages << [metadata, payload] end linux_and_x86_messages = [] @channel.queue("", :auto_delete => true).bind(exchange, :arguments => { 'x-match' => 'all', :arch => "x86", :os => 'linux' }).subscribe do |metadata, payload| linux_and_x86_messages << [metadata, payload] end any_linux_messages = [] @channel.queue("", :auto_delete => true).bind(exchange, :arguments => { "x-match" => "any", :os => 'linux' }).subscribe do |metadata, payload| any_linux_messages << [metadata, payload] end osx_or_octocore_messages = [] @channel.queue("", :auto_delete => true).bind(exchange, :arguments => { 'x-match' => 'any', :os => 'macosx', :cores => 8 }).subscribe do |metadata, payload| osx_or_octocore_messages << [metadata, payload] end riak_messages = [] @channel.queue("", :auto_delete => true).bind(exchange, :arguments => { "x-match" => "any", :package => { :name => 'riak', :version => '0.14.2' } }).subscribe do |metadata, payload| riak_messages << [metadata, payload] end EventMachine.add_timer(0.5) do exchange.publish "For linux/IA64", :headers => { :arch => "IA64", :os => 'linux' } exchange.publish "For linux/x86", :headers => { :arch => "x86", :os => 'linux' } exchange.publish "For any linux", :headers => { :os => 'linux' } exchange.publish "For OS X", :headers => { :os => 'macosx' } exchange.publish "For solaris/IA64", :headers => { :os => 'solaris', :arch => 'IA64' } exchange.publish "For ocotocore", :headers => { :cores => 8 } exchange.publish "For nodes with Riak 0.14.2", :headers => { :package => { :name => 'riak', :version => '0.14.2' } } end done(4.5) { linux_and_ia64_messages.size.should == 1 linux_and_x86_messages.size.should == 1 any_linux_messages.size.should == 3 osx_or_octocore_messages.size.should == 2 riak_messages.size.should == 1 } end end describe "Multiple consumers" do include EventedSpec::AMQPSpec default_options AMQP_OPTS default_timeout 5 describe "bound to a queue with the same single header" do # # Environment # amqp_before do @channel = AMQP::Channel.new @channel.on_error do |ch, close| raise "Channel-level error!: #{close.inspect}" end @queue = @channel.queue("", :auto_delete => true) @exchange = @channel.headers("amqpgem.tests.integration.headers.exchange1", :auto_delete => true) @queue.bind(@exchange, :arguments => { :slug => "all", "x-match" => "any" }) end it "get messages distributed to them in a round-robin manner" do mailbox1 = Array.new mailbox2 = Array.new consumer1 = AMQP::Consumer.new(@channel, @queue).consume.on_delivery { |metadata, payload| mailbox1 << payload } consumer2 = AMQP::Consumer.new(@channel, @queue).consume.on_delivery { |metadata, payload| mailbox2 << payload } EventMachine.add_timer(0.5) do 12.times { @exchange.publish(".", :headers => { :slug => "all" }) } 12.times { @exchange.publish(".", :headers => { :slug => "rspec" }) } end done(3.5) { mailbox1.size.should == 6 mailbox2.size.should == 6 } end end describe "bound to a queue with the same two header & x-match = all" do # # Environment # amqp_before do @channel = AMQP::Channel.new @channel.on_error do |ch, close| raise "Channel-level error!: #{close.inspect}" end @queue = @channel.queue("", :auto_delete => true) @exchange = @channel.headers("amqpgem.tests.integration.headers.exchange1", :auto_delete => true) @queue.bind(@exchange, :arguments => { :slug => "all", :arch => "ia64", 'x-match' => 'all' }) end it "get messages distributed to them in a round-robin manner" do mailbox1 = Array.new mailbox2 = Array.new consumer1 = AMQP::Consumer.new(@channel, @queue).consume.on_delivery { |metadata, payload| mailbox1 << payload } consumer2 = AMQP::Consumer.new(@channel, @queue).consume.on_delivery { |metadata, payload| mailbox2 << payload } EventMachine.add_timer(0.5) do 12.times { @exchange.publish(".", :headers => { :slug => "all", :arch => "ia64" }) } 12.times { @exchange.publish(".", :headers => { :slug => "rspec", :arch => "ia64" }) } end done(3.5) { mailbox1.size.should == 6 mailbox2.size.should == 6 } end end describe "bound to 2 queues with the same two header & x-match = all" do # # Environment # amqp_before do @channel = AMQP::Channel.new @channel.on_error do |ch, close| raise "Channel-level error!: #{close.inspect}" end @queue1 = @channel.queue("", :auto_delete => true) @queue2 = @channel.queue("", :auto_delete => true) @exchange = @channel.headers("amqpgem.tests.integration.headers.exchange1", :auto_delete => true) args = { :slug => "all", :arch => "ia64", 'x-match' => 'all' } @queue1.bind(@exchange, :arguments => args) @queue2.bind(@exchange, :arguments => args) end it "get messages distributed to both queues, and in a round-robin manner between consumers on one queue" do mailbox1 = Array.new mailbox2 = Array.new mailbox3 = Array.new mailbox4 = Array.new consumer1 = AMQP::Consumer.new(@channel, @queue1).consume.on_delivery { |metadata, payload| mailbox1 << payload } consumer2 = AMQP::Consumer.new(@channel, @queue1).consume.on_delivery { |metadata, payload| mailbox2 << payload } consumer3 = AMQP::Consumer.new(@channel, @queue2).consume.on_delivery { |metadata, payload| mailbox3 << payload } consumer4 = AMQP::Consumer.new(@channel, @queue2).consume.on_delivery { |metadata, payload| mailbox4 << payload } EventMachine.add_timer(0.5) do 12.times { |i| @exchange.publish("all-#{i}", :headers => { :slug => "all", :arch => "ia64" }) } 16.times { |i| @exchange.publish("rspec-#{i}", :headers => { :slug => "rspec", :arch => "ia64" }) } end done(3.5) { mailbox1.size.should == 6 mailbox1.should == ["all-0", "all-2", "all-4", "all-6", "all-8", "all-10"] mailbox2.size.should == 6 mailbox2.should == ["all-1", "all-3", "all-5", "all-7", "all-9", "all-11"] mailbox3.size.should == 6 mailbox4.size.should == 6 } end end describe "bound to a queue with the same two header & x-match = any" do # # Environment # amqp_before do @channel = AMQP::Channel.new @channel.on_error do |ch, close| raise "Channel-level error!: #{close.inspect}" end @queue = @channel.queue("", :auto_delete => true) @exchange = @channel.headers("amqpgem.tests.integration.headers.exchange1", :auto_delete => true) @queue.bind(@exchange, :arguments => { :slug => "all", :arch => "ia64", 'x-match' => 'any' }) end it "get messages distributed to them in a round-robin manner" do mailbox1 = Array.new mailbox2 = Array.new consumer1 = AMQP::Consumer.new(@channel, @queue).consume.on_delivery { |metadata, payload| mailbox1 << payload } consumer2 = AMQP::Consumer.new(@channel, @queue).consume.on_delivery { |metadata, payload| mailbox2 << payload } EventMachine.add_timer(0.5) do 12.times { @exchange.publish(".", :headers => { :slug => "all", :arch => "ia64" }) } 4.times { @exchange.publish(".", :headers => { :slug => "rspec", :arch => "ia64" }) } end done(3.5) { mailbox1.size.should == 8 mailbox2.size.should == 8 } end end end amqp-1.8.0/spec/integration/queue_declaration_spec.rb0000644000004100000410000000714013321132064023010 0ustar www-datawww-data# encoding: utf-8 require 'spec_helper' describe AMQP do # # Environment # include EventedSpec::AMQPSpec default_timeout 5 amqp_before do @channel = AMQP::Channel.new end after(:all) do AMQP.cleanup_state done end # # Examples # describe "#queue" do context "when queue name is specified" do let(:name) { "a queue declared at #{Time.now.to_i}" } it "declares a new queue with that name" do queue = @channel.queue(name) queue.name.should == name done end it "caches that queue" do queue = @channel.queue(name) @channel.queue(name).object_id.should == queue.object_id done end end # context context "when queue name is passed on as an empty string" do context "and :nowait isn't used" do it "uses server-assigned queue name" do @channel.queue("") do |queue, *args| queue.name.should_not be_empty queue.should be_anonymous queue.delete done end end end context "and :nowait is used" do it "raises ArgumentError" do expect { AMQP::Queue.new(@channel, "", :nowait => true) }.to raise_error(ArgumentError, /makes no sense/) expect { @channel.queue("", :nowait => true) }.to raise_error(ArgumentError, /makes no sense/) done end end # context end context "when queue name is nil" do it "raises ArgumentError" do expect { AMQP::Queue.new(@channel, nil) }.to raise_error(ArgumentError, /queue name must not be nil/) expect { @channel.queue(nil) }.to raise_error(ArgumentError, /queue name must not be nil/) done end end # context context "when queue is redeclared with different attributes" do let(:name) { "amqp-gem.nondurable.queue" } let(:options) { { :durable => false, :passive => false } } let(:different_options) { { :durable => true, :passive => false} } amqp_before do @queue = @channel.queue(name, options) delayed(0.25) { @queue.delete } end context "on the same channel" do it "should raise ruby exception" do expect { @channel.queue(name, different_options) }.to raise_error(AMQP::IncompatibleOptionsError) @queue.delete done end end end context "when passive option is used" do context "and queue with given name already exists" do it "silently returns" do name = "a_new_queue declared at #{Time.now.to_i}" original_queue = @channel.queue(name) queue = @channel.queue(name, :passive => true) queue.should == original_queue done end # it end context "and queue with given name DOES NOT exist" do it "raises an exception" do pending "Not yet supported" expect { exchange = @channel.queue("queue declared at #{Time.now.to_i}", :passive => true) }.to raise_error done end # it end # context end # context context "when queue is re-declared with parameters different from original declaration" do it "raises an exception" do @channel.queue("previously.declared.durable.queue", :durable => true) expect { @channel.queue("previously.declared.durable.queue", :durable => false) }.to raise_error(AMQP::IncompatibleOptionsError) done end # it end # context end # describe end # describe AMQP amqp-1.8.0/spec/integration/connection_close_spec.rb0000644000004100000410000000073113321132064022642 0ustar www-datawww-data# encoding: utf-8 require "spec_helper" describe AMQP::Session, "#close(&callback)" do include EventedSpec::AMQPSpec it "takes a callback which will run when we get back connection.close-ok" do @events = [] c = AMQP.connect do |session| @events << :open_ok session.close do |session, close_ok| @events << :close_ok end end done(0.3) { c.should be_closed @events.should == [:open_ok, :close_ok] } end end amqp-1.8.0/spec/integration/automatic_recovery_predicate_spec.rb0000644000004100000410000000235613321132064025247 0ustar www-datawww-data# encoding: utf-8 require "spec_helper" describe AMQP::Channel, "#auto_recovery" do # # Environment # include EventedSpec::AMQPSpec include EventedSpec::SpecHelper default_options AMQP_OPTS default_timeout 2 it "switches automatic recovery mode on" do ch = AMQP::Channel.new(AMQP.connection) ch.auto_recovery.should be_false ch.auto_recovery = true ch.auto_recovery.should be_true ch.auto_recovery = false ch.auto_recovery.should be_false done end end describe AMQP::Channel, "options hash" do # # Environment # include EventedSpec::AMQPSpec include EventedSpec::SpecHelper default_options AMQP_OPTS default_timeout 2 it "can be passed as the 3rd constructor argument" do ch = AMQP::Channel.new(AMQP.connection, nil, :auto_recovery => true) ch.auto_recovery.should be_true ch.auto_recovery = false ch.auto_recovery.should be_false done end it "can be passed as the 2nd constructor argument" do ch = AMQP::Channel.new(AMQP.connection, :auto_recovery => true) ch.auto_recovery.should be_true ch.should be_auto_recovering ch.auto_recovery = false ch.auto_recovery.should be_false ch.should_not be_auto_recovering done end end amqp-1.8.0/spec/integration/channel_close_spec.rb0000644000004100000410000000066413321132064022120 0ustar www-datawww-data# encoding: utf-8 require "spec_helper" describe AMQP::Channel, "#close(&callback)" do include EventedSpec::AMQPSpec it "takes a callback which will run when we get back Channel.Close-Ok" do @events = [] AMQP::Channel.new do |ch| @events << :open_ok ch.close do |channel, close_ok| @events << :close_ok end end done(0.3) { @events.should == [:open_ok, :close_ok] } end end amqp-1.8.0/spec/integration/channel_level_exception_handling_spec.rb0000644000004100000410000000264113321132064026041 0ustar www-datawww-data# encoding: utf-8 require 'spec_helper' describe "Channel-level exception" do # # Environment # include EventedSpec::AMQPSpec default_timeout 2 amqp_before do @connection = AMQP.connect @channel = AMQP::Channel.new(@connection) end after(:all) do AMQP.cleanup_state done end # # Examples # it "can be handled with Channel#on_error" do @channel.on_error do |ch, channel_close| channel_close.method_id.should == 10 channel_close.class_id.should == 50 channel_close.reply_code.should == 406 channel_close.reply_text.should_not be_nil channel_close.reply_text.should_not be_empty done end EventMachine.add_timer(0.4) do # these two definitions result in a race condition. For sake of this example, # however, it does not matter. Whatever definition succeeds first, 2nd one will # cause a channel-level exception (because attributes are not identical) # # 'a' * 80 makes queue name long enough for complete error message to be longer than 127 charachters. # makes it possible to detect signed vs unsigned integer issues in amq-protocol. MK. AMQP::Queue.new(@channel, "amqpgem.examples.channel_exception.#{'a' * 80}", :auto_delete => true, :durable => false) AMQP::Queue.new(@channel, "amqpgem.examples.channel_exception.#{'a' * 80}", :auto_delete => true, :durable => true) end end end amqp-1.8.0/spec/integration/queue_exclusivity_spec.rb0000644000004100000410000000442213321132064023113 0ustar www-datawww-data# encoding: utf-8 require 'spec_helper' describe "Non-exclusive queue" do # # Environment # include EventedSpec::EMSpec default_timeout 5 em_after { @connection1.close; @connection2.close } # # Examples # it "can be used across multiple connections" do @connection1 = AMQP.connect do @connection2 = AMQP.connect do @connection1.should_not == @connection2 channel1 = AMQP::Channel.new(@connection1) channel2 = AMQP::Channel.new(@connection2) instance1 = AMQP::Queue.new(channel1, "amqpgem.integration.queues.non-exclusive", :exclusive => false, :auto_delete => true) instance2 = AMQP::Queue.new(channel2, "amqpgem.integration.queues.non-exclusive", :exclusive => false, :auto_delete => true) exchange1 = channel1.fanout("amqpgem.integration.exchanges.fanout1", :auto_delete => true) exchange2 = channel2.fanout("amqpgem.integration.exchanges.fanout2", :auto_delete => true) instance1.bind(exchange1).subscribe do |payload| end instance2.bind(exchange2).subscribe do |payload| end done(0.2) { channel1.should be_open channel1.close channel2.should be_open channel2.close } end end end end describe "Exclusive queue" do # # Environment # include EventedSpec::EMSpec default_timeout 2 em_after { @connection1.close; @connection2.close } # # Examples # it "can ONLY be used by ONE connection" do @connection1 = AMQP.connect do @connection2 = AMQP.connect do @connection1.should_not == @connection2 queue_name = "amqpgem.integration.queues.exclusive" callback_fired = false @channel1 = AMQP::Channel.new(@connection1) do AMQP::Queue.new(@channel1, queue_name, :exclusive => true) do @channel2 = AMQP::Channel.new(@connection2) do AMQP::Queue.new(@channel2, queue_name, :exclusive => true) do callback_fired = true end end end end done(1) { callback_fired.should be_false @channel1.should_not be_closed # because it is a channel-level exception @channel2.should be_closed } end end end end amqp-1.8.0/spec/integration/ordering_of_published_messages_spec.rb0000644000004100000410000000410013321132064025533 0ustar www-datawww-data# encoding: utf-8 require 'spec_helper' describe "100 AMQP messages" do # # Environment # include EventedSpec::AMQPSpec default_options AMQP_OPTS default_timeout 5 before :all do @list = Range.new(0, 100, true).to_a end context "published and received on the same channel" do amqp_before do @channel = AMQP::Channel.new @channel.on_error do |ch, close| raise "Channel-level error!: #{close.inspect}" end @queue = @channel.queue("amqpgem.tests.integration.queue1", :auto_delete => true) end # # Examples # it "are received on the same channel in the order of publishing" do received = [] @queue.subscribe do |metadata, payload| received << payload.to_i end EventMachine.add_timer(0.3) do @list.each { |i| @channel.default_exchange.publish(i.to_s, :routing_key => @queue.name) } end done(3.5) { received.size.should == 100 received.first.should == 0 received.last.should == 99 received.should == @list } end end context "published on two different channels" do amqp_before do @channel1 = AMQP::Channel.new @channel2 = AMQP::Channel.new @channel1.on_error do |ch, close| raise "Channel-level error!: #{close.inspect}" end @channel2.on_error do |ch, close| raise "Channel-level error!: #{close.inspect}" end @queue = @channel1.queue("amqpgem.tests.integration.queue1", :auto_delete => true) end # # Examples # it "are received on the same channel in the order of publishing" do received = [] @queue.subscribe do |metadata, payload| received << payload.to_i end EventMachine.add_timer(0.3) do @list.each { |i| @channel2.default_exchange.publish(i.to_s, :routing_key => @queue.name) } end done(3.5) { received.size.should == 100 received.first.should == 0 received.last.should == 99 received.should == @list } end end end # describe amqp-1.8.0/spec/integration/exchange_to_exchange_binding_spec.rb0000644000004100000410000000736113321132064025144 0ustar www-datawww-data# encoding: utf-8 require 'spec_helper' describe AMQP::Channel do # # Environment # include EventedSpec::AMQPSpec default_options AMQP_OPTS default_timeout 2 amqp_before do @channel = AMQP::Channel.new end # # Examples # describe "exchange to exchange bindings" do context "basic properties" do it "bind returns self" do source1 = @channel.fanout("fanout-exchange-source-1") source2 = @channel.fanout("fanout-exchange-source-2") destination = @channel.fanout("fanout-exchange-destination-multi") destination.bind(source1).bind(source2).should == destination done end it "bind can work with strings for source exchange parameter" do @channel.fanout("fanout-exchange-source-3") destination = @channel.fanout("fanout-exchange-destination-multi-2") destination.bind('fanout-exchange-source-3').should == destination done end end context "fanout exchanges" do it "are bindable and forward messages" do source = @channel.fanout("fanout-exchange-source") destination = @channel.fanout("fanout-exchange-destination") messages = [] destination.bind(source) do queue = @channel.queue("fanout-forwarding", :auto_delete => true) queue.subscribe do |metadata, payload| messages << payload end queue.bind(destination) do source.publish("x") end end done(0.5) { messages.should == ["x"] } end it "can be unbound" do source = @channel.fanout("fanout-exchange-source-2") destination = @channel.fanout("fanout-exchange-destination-2") destination.bind(source) do destination.unbind(source) do done end end end it "can be unbound without callbacks" do source = @channel.fanout("fanout-exchange-source-2") destination = @channel.fanout("fanout-exchange-destination-2") destination.bind(source) destination.unbind(source) done end it "using :nowait => true should not call a passed in block" do source = @channel.fanout("fanout-exchange-source-no-wait") destination = @channel.fanout("fanout-exchange-destination-no-wait") callback_called = false destination.bind(source, :nowait => true) do callback_called = true end done(0.5) { callback_called.should be_false} end end #context context "topic exchanges" do it "using routing key '#'" do source = @channel.topic("topic-exchange-source") destination = @channel.topic("topic-exchange-destination") messages = [] destination.bind(source, :routing_key => "#") do queue = @channel.queue("ex-to-ex-default-key", :auto_delete => true) queue.bind(destination, :routing_key => "#").subscribe do |metadata, payload| messages << payload end source.publish("a", :routing_key => "lalalala") end done(0.5) { messages.should == ["a"] } end it "using routing key 'foo'" do source = @channel.topic("topic-exchange-source-foo") destination = @channel.topic("topic-exchange-destination-foo") messages = [] destination.bind(source, :routing_key => 'foo') do queue = @channel.queue("foo-foo", :auto_delete => true) queue.subscribe do |metadata, payload| messages << payload end queue.bind(destination, :routing_key => "foo") do source.publish("b", :routing_key => "foo") end end done(0.5) { messages.should == ["b"]} end end #context end # describe end # describe AMQP amqp-1.8.0/spec/integration/direct_exchange_routing_spec.rb0000644000004100000410000000600413321132064024200 0ustar www-datawww-data# encoding: utf-8 require 'spec_helper' describe "Multiple consumers bound to a queue with the same routing key" do # # Environment # include EventedSpec::AMQPSpec default_options AMQP_OPTS default_timeout 5 amqp_before do @channel = AMQP::Channel.new @channel.on_error do |ch, close| raise "Channel-level error!: #{close.inspect}" end @queue = @channel.queue("", :auto_delete => true) @exchange = @channel.direct("amqpgem.tests.integration.direct.exchange", :auto_delete => true) @queue.bind(@exchange, :routing_key => "builds.all") end it "get messages distributed to them in a round-robin manner" do mailbox1 = Array.new mailbox2 = Array.new consumer1 = AMQP::Consumer.new(@channel, @queue).consume consumer2 = AMQP::Consumer.new(@channel, @queue).consume consumer1.on_delivery do |metadata, payload| mailbox1 << payload end consumer2.on_delivery do |metadata, payload| mailbox2 << payload end EventMachine.add_timer(0.5) do 12.times { @exchange.publish(".", :routing_key => "builds.all") } 12.times { @exchange.publish(".", :routing_key => "all.builds") } end done(4.5) { mailbox1.size.should == 6 mailbox2.size.should == 6 } end end describe "Multiple queues bound to a direct exchange with the same routing key" do # # Environment # include EventedSpec::AMQPSpec default_options AMQP_OPTS default_timeout 5 amqp_before do @channel = AMQP::Channel.new @channel.on_error do |ch, close| raise "Channel-level error!: #{close.inspect}" end @queue1 = @channel.queue("", :auto_delete => true) @queue2 = @channel.queue("", :auto_delete => true) @exchange = @channel.direct("amqpgem.tests.integration.direct.exchange", :auto_delete => true) @queue1.bind(@exchange, :routing_key => "builds.all") @queue2.bind(@exchange, :routing_key => "builds.all") end it "all get a copy of messages with that routing key" do mailbox1 = Array.new mailbox2 = Array.new mailbox3 = Array.new mailbox4 = Array.new consumer1 = AMQP::Consumer.new(@channel, @queue1).consume consumer2 = AMQP::Consumer.new(@channel, @queue1).consume consumer3 = AMQP::Consumer.new(@channel, @queue2).consume consumer4 = AMQP::Consumer.new(@channel, @queue2).consume consumer1.on_delivery do |metadata, payload| mailbox1 << payload end consumer2.on_delivery do |metadata, payload| mailbox2 << payload end consumer3.on_delivery do |metadata, payload| mailbox3 << payload end consumer4.on_delivery do |metadata, payload| mailbox4 << payload end EventMachine.add_timer(0.5) do 13.times { @exchange.publish(".", :routing_key => "builds.all") } 13.times { @exchange.publish(".", :routing_key => "all.builds") } end done(3.5) { mailbox1.size.should == 7 mailbox2.size.should == 6 mailbox3.size.should == 7 mailbox4.size.should == 6 } end end amqp-1.8.0/spec/integration/basic_get_spec.rb0000644000004100000410000000670013321132064021240 0ustar www-datawww-data# encoding: utf-8 require "spec_helper" describe AMQP::Queue, "#pop" do # # Environment # include EventedSpec::AMQPSpec include EventedSpec::SpecHelper default_options AMQP_OPTS default_timeout 10 amqp_before do @channel = AMQP::Channel.new @channel.should be_open @queue_name = "amqpgem.integration.basic.get.queue" @exchange = @channel.fanout("amqpgem.integration.basic.get.queue", :auto_delete => true) @queue = @channel.queue(@queue_name, :auto_delete => true) @queue.bind(@exchange) @dispatched_data = "fetch me synchronously" end # # Examples # context "when THERE ARE NO messages in the queue" do it "yields nil (instead of message payload) to the callback" do @queue.purge do callback_has_fired = false @queue.status do |number_of_messages, number_of_consumers| number_of_messages.should == 0 end @queue.pop do |payload| callback_has_fired = true @queue.delete payload.should be_nil end done(0.2) { callback_has_fired.should be_true } end end end context "when THERE ARE messages in the queue" do it "yields message payload to the callback" do @channel.default_exchange.publish(@dispatched_data, :routing_key => @queue.name) @queue.pop do |headers, payload| payload.should == @dispatched_data end delayed(0.5) { # Queue.Get doesn't qualify for subscription, hence, manual deletion is required @queue.delete } done(0.8) end # it end # context context "with manual acknowledgements" do default_timeout 4 let(:queue_name) { "amqpgem.integration.basic.get.acks.manual#{rand}" } it "does not remove messages from the queue unless ack-ed" do ch1 = AMQP::Channel.new ch2 = AMQP::Channel.new ch1.on_error do |ch, close_ok| puts "Channel error: #{close_ok.reply_code} #{close_ok.reply_text}" end q = ch1.queue(queue_name, :exclusive => true) x = ch1.default_exchange q.purge delayed(0.2) { x.publish(@dispatched_data, :routing_key => q.name) } delayed(0.5) { q.pop(:ack => true) do |meta, payload| # never ack end ch1.close EventMachine.add_timer(0.7) { ch2.queue(queue_name, :exclusive => true).status do |number_of_messages, number_of_consumers| number_of_messages.should == 1 done end } } end end context "with automatic acknowledgements" do default_timeout 4 let(:queue_name) { "amqpgem.integration.basic.get.acks.automatic#{rand}" } it "does remove messages from the queue after delivery" do ch1 = AMQP::Channel.new ch2 = AMQP::Channel.new ch1.on_error do |ch, close_ok| puts "Channel error: #{close_ok.reply_code} #{close_ok.reply_text}" end q = ch1.queue(queue_name, :exclusive => true) x = ch1.default_exchange q.purge x.publish(@dispatched_data, :routing_key => q.name) delayed(0.5) { q.pop(:ack => false) do |meta, payload| # never ack end ch1.close EventMachine.add_timer(0.5) { ch2.queue(queue_name, :exclusive => true).status do |number_of_messages, number_of_consumers| number_of_messages.should == 0 done end } } end end end # describe amqp-1.8.0/spec/integration/mandatory_messages_spec.rb0000644000004100000410000000223313321132064023202 0ustar www-datawww-data# encoding: utf-8 require 'spec_helper' describe "When exchange a message is published to has no bindings" do # # Environment # include EventedSpec::AMQPSpec default_timeout 1.5 amqp_before do @producer_channel = AMQP::Channel.new @consumer_channel = AMQP::Channel.new end # ... # # Examples # context "and message is published as :mandatory" do it "that message is returned to the publisher" do exchange = @producer_channel.fanout("amqpgem.examples.mandatory.messages", :auto_delete => true) exchange.on_return do |basic_return, metadata, payload| done if payload == "mandatory message body" end exchange.publish "mandatory message body", :mandatory => true end end context "and message is published as non :mandatory" do it "that message is dropped" do exchange = @producer_channel.fanout("amqpgem.examples.mandatory.messages", :auto_delete => true) exchange.on_return do |basic_return, metadata, payload| fail "Should not happen" end exchange.publish "mandatory message body", :mandatory => false done(1.0) end end end # describe amqp-1.8.0/spec/integration/channel_level_exception_with_multiple_channels_spec.rb0000644000004100000410000000247013321132064031016 0ustar www-datawww-data# encoding: utf-8 require 'spec_helper' describe AMQP do # # Environment # include EventedSpec::AMQPSpec default_timeout 5 # # Examples # context "when queue is redeclared with different attributes across two channels" do let(:name) { "amqp-gem.nondurable.queue" } let(:options) { { :durable => false, :passive => false } } let(:different_options) { { :durable => true, :passive => false } } it "should trigger channel-level #on_error callback" do @channel = AMQP::Channel.new @channel.on_error do |ch, close| puts "This should never happen" end @q1 = @channel.queue(name, options) # Small delays to ensure the order of execution delayed(0.1) { @other_channel = AMQP::Channel.new @other_channel.on_error do |ch, close| @callback_fired = true end puts "other_channel.id = #{@other_channel.id}" @q2 = @other_channel.queue(name, different_options) } delayed(0.3) { @q2.delete } done(0.4) { @callback_fired.should be_true # looks like there is a difference between platforms/machines # so check either one. MK. @other_channel.closed?.should be_true } end end end # describe AMQP amqp-1.8.0/spec/integration/queue_status_spec.rb0000644000004100000410000000262113321132064022045 0ustar www-datawww-data# encoding: utf-8 require 'spec_helper' describe AMQP::Queue do # # Environment # include EventedSpec::AMQPSpec default_timeout 5 # # Examples # describe "#status" do it "yields # of messages & consumers to the callback" do events = [] channel = AMQP::Channel.new queue1 = channel.queue("", :auto_delete => true) queue2 = channel.queue("amqpgem.tests.a.named.queue", :auto_delete => true) EventMachine.add_timer(1.0) do queue1.status do |m, c| events << :queue1_declare_ok end queue2.status do |m, c| events << :queue2_declare_ok end end done(2.0) { events.should include(:queue1_declare_ok) events.should include(:queue2_declare_ok) } end it "yields # of messages & consumers to the callback in pseudo-synchronous code" do events = [] channel = AMQP::Channel.new queue1 = channel.queue("", :auto_delete => true) queue2 = channel.queue("amqpgem.tests.a.named.queue", :auto_delete => true) queue1.status do |m, c| events << :queue1_declare_ok end queue2.status do |m, c| events << :queue2_declare_ok end done(2.0) { queue1.name.should =~ /^amq\..+/ events.should include(:queue1_declare_ok) events.should include(:queue2_declare_ok) } end end end amqp-1.8.0/spec/integration/automatic_binding_for_default_direct_exchange_spec.rb0000644000004100000410000000331313321132064030543 0ustar www-datawww-data# encoding: utf-8 require 'spec_helper' describe "Queue that was bound to default direct exchange thanks to Automatic Mode (section 2.1.2.4 in AMQP 0.9.1 spec" do # # Environment # include EventedSpec::AMQPSpec default_timeout 3 amqp_before do @channel = AMQP::Channel.new @channel.should be_open @channel.on_error do |ch, close| raise "Channel-level error!: #{close.inspect}" end @queue1 = @channel.queue("amqpgem.tests.integration.queue1", :auto_delete => true) @queue2 = @channel.queue("amqpgem.tests.integration.queue2", :auto_delete => true) # Rely on default direct exchange binding, see section 2.1.2.4 Automatic Mode in AMQP 0.9.1 spec. @exchange = AMQP::Exchange.default(@channel) end default_options AMQP_OPTS # # Examples # it "receives messages with routing key equals it's name" do @exchange.channel.should == @channel number_of_received_messages = 0 expected_number_of_messages = 3 dispatched_data = "to be received by queue1" @queue1.subscribe do |payload| number_of_received_messages += 1 payload.should == dispatched_data end # subscribe 4.times do @exchange.publish("some white noise", :routing_key => "killa key") end expected_number_of_messages.times do @exchange.publish(dispatched_data, :routing_key => @queue1.name) end 4.times do @exchange.publish("some white noise", :routing_key => "killa key") end delayed(0.6) { # We never subscribe to it, hence, need to delete manually @queue2.delete } done(0.9) { number_of_received_messages.should == expected_number_of_messages } end # it end # describe amqp-1.8.0/spec/integration/extensions/0000755000004100000410000000000013321132064020155 5ustar www-datawww-dataamqp-1.8.0/spec/integration/extensions/rabbitmq/0000755000004100000410000000000013321132064021756 5ustar www-datawww-dataamqp-1.8.0/spec/integration/extensions/rabbitmq/publisher_confirmations_spec.rb0000644000004100000410000001052613321132064030251 0ustar www-datawww-data# encoding: utf-8 require "spec_helper" require "amqp/extensions/rabbitmq" describe "confirm.select" do # # Environment # include EventedSpec::AMQPSpec include EventedSpec::SpecHelper default_options AMQP_OPTS default_timeout 3 amqp_before do @channel = AMQP::Channel.new end context "with :nowait attribute off" do it "results in a confirm.select-ok response" do @channel.confirm_select do |select_ok| done end end end end describe "Publisher confirmation(s)" do # # Environment # include EventedSpec::AMQPSpec include EventedSpec::SpecHelper default_options AMQP_OPTS default_timeout 3 amqp_before do @channel1 = AMQP::Channel.new @channel2 = AMQP::Channel.new end it 'should increment publisher_index confirming channel' do channel3 = AMQP::Channel.new exchange = channel3.fanout("amqpgem.tests.fanout0", :auto_delete => true) channel3.confirm_select channel3.publisher_index.should == 0 EventMachine.add_timer(0.5) do exchange.publish("Hi") end done(2.0) do channel3.publisher_index.should == 1 end end context "when messages are transient" do context "and routable" do it "are confirmed as soon as they arrive on all the queues they were routed to" do events = Array.new exchange = @channel2.fanout("amqpgem.tests.fanout1", :auto_delete => true) queue = @channel1.queue("", :auto_delete => true).bind(exchange).subscribe do |metadata, payload| events << :basic_delivery end @channel2.confirm_select @channel2.on_ack do |basic_ack| events << :basic_ack end exchange.on_return do |basic_return, metadata, payload| fail "Should never happen" end EventMachine.add_timer(0.5) do exchange.publish("Hi", :persistent => false, :mandatory => true) end done(2.0) do events.should include(:basic_ack) events.should include(:basic_delivery) end end end context "and can be delivered immediately" do it "are confirmed as soon as they arrive on all the queues they were routed to" do events = Array.new exchange = @channel2.fanout("amqpgem.tests.fanout2", :auto_delete => true) queue = @channel1.queue("", :auto_delete => true).bind(exchange).subscribe do |metadata, payload| events << :basic_delivery end @channel2.confirm_select @channel2.on_ack do |basic_ack| events << :basic_ack end exchange.on_return do |basic_return, metadata, payload| fail "Should never happen" end EventMachine.add_timer(0.5) do exchange.publish("Hi", :persistent => false) end done(2.0) do events.should include(:basic_ack) events.should include(:basic_delivery) end end end context "and NOT routable" do it "are delivered immediately after basic.return" do events = Array.new queue = @channel1.queue("", :auto_delete => true) exchange = @channel2.fanout("amqpgem.tests.fanout3", :auto_delete => true) @channel2.confirm_select @channel2.on_ack do |basic_ack| events << :basic_ack end exchange.on_return do |basic_return, metadata, payload| events << :basic_return end EventMachine.add_timer(0.5) do exchange.publish("Hi", :persistent => false, :mandatory => true) end done(2.0) { events.should == [:basic_return, :basic_ack] } end end context "and CAN NOT be delivered immediately" do it "are delivered immediately after basic.return" do events = Array.new queue = @channel1.queue("", :auto_delete => true) exchange = @channel2.fanout("amqpgem.tests.fanout4", :auto_delete => true) @channel2.confirm_select @channel2.on_ack do |basic_ack| events << :basic_ack end exchange.on_return do |basic_return, metadata, payload| events << :basic_return end EventMachine.add_timer(0.5) do exchange.publish("Hi", :persistent => false, :mandatory => true) end done(2.0) { events.should == [:basic_return, :basic_ack] } end end end end amqp-1.8.0/spec/integration/exchange_declaration_spec.rb0000644000004100000410000004172113321132064023451 0ustar www-datawww-data# encoding: utf-8 require 'spec_helper' describe AMQP::Channel do # # Environment # include EventedSpec::AMQPSpec default_options AMQP_OPTS default_timeout 2 amqp_before do @channel = AMQP::Channel.new end # # Examples # describe "default exchange" do subject do @channel.default_exchange end it "is predefined" do subject.should be_predefined done end end describe "exchange named amq.direct" do subject do @channel.direct("amq.direct") end it "is predefined" do subject.should be_predefined done end end describe "exchange named amq.fanout" do subject do @channel.direct("amq.fanout") end it "is predefined" do subject.should be_predefined done end end describe "exchange named amq.topic" do subject do @channel.direct("amq.topic") end it "is predefined" do subject.should be_predefined done end end describe "exchange named amq.match" do subject do @channel.direct("amq.match") end it "is predefined" do subject.should be_predefined done end end describe "exchange named amq.headers" do subject do @channel.direct("amq.headers") end it "is predefined" do subject.should be_predefined done end end describe "#direct" do context "when exchange name is specified" do it 'declares a new direct exchange with that name' do @channel.direct('name').name.should == 'name' done end it "declares direct exchange as transient (non-durable)" do exchange = @channel.direct('name') exchange.should_not be_durable exchange.should be_transient done end it "declares direct exchange as non-auto-deleted" do exchange = @channel.direct('name') exchange.should_not be_auto_deleted done end end context "when exchange name is omitted" do it 'uses amq.direct' do @channel.direct.name.should == 'amq.direct' done end # it end # context context "when passive option is used" do context "and exchange with given name already exists" do it "silently returns" do name = "a_new_direct_exchange declared at #{Time.now.to_i}" channel = AMQP::Channel.new original_exchange = channel.direct(name) exchange = channel.direct(name, :passive => true) exchange.should == original_exchange done end # it end context "and exchange with given name DOES NOT exist" do it "raises an exception" do channel = AMQP::Channel.new channel.on_error do |ch, channel_close| @error_code = channel_close.reply_code end exchange = channel.direct("direct exchange declared at #{Time.now.to_i}", :passive => true) done(0.5) { @error_code.should == 404 } end # it end # context end # context context "when exchange is declared as durable" do it "returns a new durable direct exchange" do exchange = @channel.direct("a_new_durable_direct_exchange", :durable => true) exchange.should be_durable exchange.should_not be_transient done end # it end # context context "when exchange is declared as non-durable" do it "returns a new NON-durable direct exchange" do exchange = @channel.direct("a_new_non_durable_direct_exchange", :durable => false) exchange.should_not be_durable exchange.should be_transient done end # it end # context context "when exchange is declared as auto-deleted" do it "returns a new auto-deleted direct exchange" do exchange = @channel.direct("a new auto-deleted direct exchange", :auto_delete => true) exchange.should be_auto_deleted done end # it end # context context "when exchange is declared as auto-deleted" do it "returns a new auto-deleted direct exchange" do exchange = @channel.direct("a new non-auto-deleted direct exchange", :auto_delete => false) exchange.should_not be_auto_deleted done end # it end # context context "when exchange is declared without explicit :nowait parameter" do it "is declared with :nowait by default" do exchange = @channel.direct("a new non-auto-deleted direct exchange", :auto_delete => false) exchange.should_not be_auto_deleted done end # it end # context context "when exchange is re-declared with parameters different from original declaration" do it "raises an exception" do channel = AMQP::Channel.new channel.direct("previously.declared.durable.direct.exchange", :durable => true) expect { channel.direct("previously.declared.durable.direct.exchange", :durable => false) }.to raise_error(AMQP::IncompatibleOptionsError) done end # it end # context end # describe describe "#fanout" do context "when exchange name is specified" do let(:name) { "new.fanout.exchange" } it "declares a new fanout exchange with that name" do exchange = @channel.fanout(name) exchange.name.should == name done end end # context context "when exchange name is specified and :nowait is false" do let(:name) { "new.fanout.exchange#{rand}" } it "declares a new fanout exchange with that name" do exchange = @channel.fanout(name, :nowait => false) exchange.delete done(0.5) { exchange.name.should == name } end end # context context "when exchange name is omitted" do it "uses amq.fanout" do exchange = @channel.fanout exchange.name.should == "amq.fanout" exchange.name.should_not == "amq.fanout2" done end end # context context "when passive option is used" do context "and exchange with given name already exists" do it "silently returns" do name = "a_new_fanout_exchange declared at #{Time.now.to_i}" original_exchange = @channel.fanout(name) exchange = @channel.fanout(name, :passive => true) exchange.should == original_exchange done end # it end context "and exchange with given name DOES NOT exist" do it "results in a channel exception" do channel = AMQP::Channel.new channel.on_error do |ch, channel_close| @error_code = channel_close.reply_code end exchange = channel.fanout("fanout exchange declared at #{Time.now.to_i}", :passive => true) done(0.5) { @error_code.should == 404 } done end # it end # context end # context context "when exchange is declared as durable" do it "returns a new durable fanout exchange" do exchange = @channel.fanout("a_new_durable_fanout_exchange", :durable => true) exchange.should be_durable exchange.should_not be_transient done end # it end # context context "when exchange is declared as non-durable" do it "returns a new NON-durable fanout exchange" do exchange = @channel.fanout("a_new_non_durable_fanout_exchange", :durable => false) exchange.should_not be_durable exchange.should be_transient done end # it end # context context "when exchange is declared as auto-deleted" do it "returns a new auto-deleted fanout exchange" do exchange = @channel.fanout("a new auto-deleted fanout exchange", :auto_delete => true) exchange.should be_auto_deleted done end # it end # context context "when exchange is declared as auto-deleted" do it "returns a new auto-deleted fanout exchange" do exchange = @channel.fanout("a new non-auto-deleted fanout exchange", :auto_delete => false) exchange.should_not be_auto_deleted done end # it end # context context "when exchange is declared without explicit :nowait parameter" do it "is declared with :nowait by default" do exchange = @channel.fanout("a new non-auto-deleted fanout exchange", :auto_delete => false) exchange.should_not be_auto_deleted done end # it end # context context "when exchange is re-declared with parameters different from original declaration" do it "raises an exception" do channel = AMQP::Channel.new channel.fanout("previously.declared.durable.topic.exchange", :durable => true) expect { channel.fanout("previously.declared.durable.topic.exchange", :durable => false) }.to raise_error(AMQP::IncompatibleOptionsError) done end # it end # context end # describe describe "#topic" do context "when exchange name is specified" do let(:name) { "a.topic.exchange" } it "declares a new topic exchange with that name" do exchange = @channel.topic(name) exchange.name.should == name done end # it end # context context "when exchange name is omitted" do it "uses amq.topic" do exchange = @channel.topic exchange.name.should == "amq.topic" exchange.name.should_not == "amq.topic2" done end # it end # context context "when passive option is used" do context "and exchange with given name already exists" do it "silently returns" do name = "a_new_topic_exchange declared at #{Time.now.to_i}" original_exchange = @channel.topic(name) exchange = @channel.topic(name, :passive => true) exchange.should == original_exchange done end # it end # context context "and exchange with given name DOES NOT exist" do it "results in a channel exception" do channel = AMQP::Channel.new channel.on_error do |ch, channel_close| @error_code = channel_close.reply_code end exchange = channel.topic("topic exchange declared at #{Time.now.to_i}", :passive => true) done(0.5) { @error_code.should == 404 } end # it end # context end context "when exchange is declared as durable" do it "returns a new durable topic exchange" do exchange = @channel.topic("a_new_durable_topic_exchange", :durable => true) exchange.should be_durable exchange.should_not be_transient done end # it end # context context "when exchange is declared as non-durable" do it "returns a new NON-durable topic exchange" do exchange = @channel.topic("a_new_non_durable_topic_exchange", :durable => false) exchange.should_not be_durable exchange.should be_transient done end # it end # context context "when exchange is declared as auto-deleted" do it "returns a new auto-deleted topic exchange" do exchange = @channel.topic("a new auto-deleted topic exchange", :auto_delete => true) exchange.should be_auto_deleted done end # it end # context context "when exchange is declared as auto-deleted" do it "returns a new auto-deleted topic exchange" do exchange = @channel.topic("a new non-auto-deleted topic exchange", :auto_delete => false) exchange.should_not be_auto_deleted done end # it end # context context "when exchange is declared without explicit :nowait parameter" do it "is declared with :nowait by default" do exchange = @channel.topic("a new non-auto-deleted topic exchange", :auto_delete => false) exchange.should_not be_auto_deleted done end # it end # context context "when exchange is declared with or without :internal parameter" do it "should create a public exchange by default" do exchange = @channel.topic("a new public topic exchange") exchange.should_not be_internal done end # it it "should create a public exchange when :internal is false" do exchange = @channel.topic("a new-public topic exchange", :internal => false) exchange.should_not be_internal done end # it it "should create an internal exchange when :internal is true" do exchange = @channel.topic("a new internal topic exchange", :internal => true) exchange.should be_internal done end # it end # context context "when exchange is re-declared with parameters different from the original declaration" do amqp_after do done end it "raises an exception" do channel = AMQP::Channel.new channel.topic("previously.declared.durable.topic.exchange", :durable => true) expect { channel.topic("previously.declared.durable.topic.exchange", :durable => false) }.to raise_error(AMQP::IncompatibleOptionsError) done end # it end # context end # describe describe "#headers" do context "when exchange name is specified" do let(:name) { "new.headers.exchange" } it "declares a new headers exchange with that name" do channel = AMQP::Channel.new exchange = channel.headers(name) exchange.name.should == name done end end # context context "when exchange name is omitted" do amqp_after do done end it "uses amq.match" do channel = AMQP::Channel.new exchange = channel.headers exchange.name.should == "amq.match" exchange.name.should_not == "amq.headers" done end end # context context "when passive option is used" do context "and exchange with given name already exists" do it "silently returns" do name = "a_new_headers_exchange declared at #{Time.now.to_i}" channel = AMQP::Channel.new original_exchange = channel.headers(name) exchange = channel.headers(name, :passive => true) exchange.should == original_exchange done end # it end context "and exchange with given name DOES NOT exist" do it "raises an exception" do channel = AMQP::Channel.new channel.on_error do |ch, channel_close| @error_code = channel_close.reply_code end exchange = channel.headers("headers exchange declared at #{Time.now.to_i}", :passive => true) done(0.5) { @error_code.should == 404 } end # it end # context end # context context "when exchange is declared as durable" do it "returns a new durable headers exchange" do channel = AMQP::Channel.new exchange = channel.headers("a_new_durable_headers_exchange", :durable => true) exchange.should be_durable exchange.should_not be_transient done end # it end # context context "when exchange is declared as non-durable" do it "returns a new NON-durable headers exchange" do channel = AMQP::Channel.new exchange = channel.headers("a_new_non_durable_headers_exchange", :durable => false) exchange.should_not be_durable exchange.should be_transient done end # it end # context context "when exchange is declared as auto-deleted" do it "returns a new auto-deleted headers exchange" do channel = AMQP::Channel.new exchange = channel.headers("a new auto-deleted headers exchange", :auto_delete => true) exchange.should be_auto_deleted done end # it end # context context "when exchange is declared as auto-deleted" do it "returns a new auto-deleted headers exchange" do channel = AMQP::Channel.new exchange = channel.headers("a new non-auto-deleted headers exchange", :auto_delete => false) exchange.should_not be_auto_deleted done end # it end # context context "when exchange is declared without explicit :nowait parameter" do it "is declared with :nowait by default" do channel = AMQP::Channel.new exchange = channel.headers("a new non-auto-deleted headers exchange", :auto_delete => false) exchange.should_not be_auto_deleted done end # it end # context context "when exchange is re-declared with parameters different from original declaration on the same channel" do amqp_after do done end it "raises an exception" do channel = AMQP::Channel.new channel.headers("previously.declared.durable.headers.exchange", :durable => true) expect { channel.headers("previously.declared.durable.headers.exchange", :durable => false) }.to raise_error(AMQP::IncompatibleOptionsError) done end # it end # context end # describe end # describe AMQP amqp-1.8.0/.gitignore0000644000004100000410000000022213321132064014465 0ustar www-datawww-data/*.gem *.rbc .rbx/* ~* #* *~ .rvmrc .bundle Gemfile.lock spec/amqp.yml vendor .yardoc/* doc/* tmp/* bin/* .AppleDouble/* debug/* .ruby-version amqp-1.8.0/examples/0000755000004100000410000000000013321132064014317 5ustar www-datawww-dataamqp-1.8.0/examples/exchanges/0000755000004100000410000000000013321132064016264 5ustar www-datawww-dataamqp-1.8.0/examples/exchanges/autodeletion_of_exchanges.rb0000644000004100000410000000156013321132064024020 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' puts "=> Exchange#initialize example that uses :auto_delete => true" puts AMQP.start(:host => 'localhost', :port => 5673) do |connection| AMQP::Channel.new do |channel, open_ok| puts "Channel ##{channel.id} is now open!" AMQP::Exchange.new(channel, :direct, "amqpgem.examples.xchange2", :auto_delete => false) do |exchange| puts "#{exchange.name} is ready to go" end AMQP::Exchange.new(channel, :direct, "amqpgem.examples.xchange3", :auto_delete => true) do |exchange| puts "#{exchange.name} is ready to go" end end show_stopper = Proc.new do $stdout.puts "Stopping..." connection.close { EventMachine.stop } end Signal.trap "INT", show_stopper EM.add_timer(2, show_stopper) end amqp-1.8.0/examples/exchanges/declare_an_exchange_without_assignment.rb0000644000004100000410000000211013321132064026535 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' puts "=> Exchange#initialize example that uses a block" puts AMQP.start(:host => 'localhost') do |connection| AMQP::Channel.new do |channel, open_ok| puts "Channel ##{channel.id} is now open!" AMQP::Exchange.new(channel, :direct, "amqpgem.examples.xchange1", :auto_delete => true) do |exchange| puts "#{exchange.name} is ready to go" end AMQP::Exchange.new(channel, :direct, "amqpgem.examples.xchange1", :auto_delete => true) do |exchange, declare_ok| puts "#{exchange.name} is ready to go. AMQP method: #{declare_ok.inspect}" end channel.direct("amqpgem.examples.xchange2", :auto_delete => true) do |exchange, declare_ok| puts "#{exchange.name} is ready to go. AMQP method: #{declare_ok.inspect}" end end show_stopper = Proc.new do $stdout.puts "Stopping..." connection.close { EM.stop { exit } } end Signal.trap "INT", show_stopper EM.add_timer(2, show_stopper) end amqp-1.8.0/examples/patterns/0000755000004100000410000000000013321132064016157 5ustar www-datawww-dataamqp-1.8.0/examples/patterns/command/0000755000004100000410000000000013321132064017575 5ustar www-datawww-dataamqp-1.8.0/examples/patterns/command/consumer.rb0000644000004100000410000000246613321132064021765 0ustar www-datawww-data# encoding: utf-8 $LOAD_PATH.unshift File.expand_path("../../../../lib", __FILE__) require "amqp" require "yaml" t = Thread.new { EventMachine.run } sleep(0.5) connection = AMQP.connect channel = AMQP::Channel.new(connection, :auto_recovery => true) channel.prefetch(1) # Acknowledgements are good for letting the server know # that the task is finished. If the consumer doesn't send # the acknowledgement, then the task is considered to be unfinished # and will be requeued when consumer closes AMQP connection (because of a crash, for example). channel.queue("amqpgem.examples.patterns.command", :durable => true, :auto_delete => false).subscribe(:ack => true) do |metadata, payload| case metadata.type when "gems.install" data = YAML.load(payload) puts "[gems.install] Received a 'gems.install' request with #{data.inspect}" # just to demonstrate a realistic example shellout = "gem install #{data[:gem]} --version '#{data[:version]}'" puts "[gems.install] Executing #{shellout}"; system(shellout) puts puts "[gems.install] Done" puts else puts "[commands] Unknown command: #{metadata.type}" end # message is processed, acknowledge it so that broker discards it metadata.ack end puts "[boot] Ready" Signal.trap("INT") { connection.close { EventMachine.stop } } t.join amqp-1.8.0/examples/patterns/command/producer.rb0000644000004100000410000000137513321132064021753 0ustar www-datawww-data# encoding: utf-8 $LOAD_PATH.unshift File.expand_path("../../../../lib", __FILE__) require "amqp" require "yaml" t = Thread.new { EventMachine.run } sleep(0.5) connection = AMQP.connect channel = AMQP::Channel.new(connection) # publish new commands every few seconds EventMachine.add_periodic_timer(10.0) do puts "Publishing a command (gems.install)" payload = { :gem => "rack", :version => "~> 1.3.0" }.to_yaml channel.default_exchange.publish(payload, :type => "gems.install", :routing_key => "amqpgem.examples.patterns.command") end puts "[boot] Ready. Will be publishing commands every 10 seconds." Signal.trap("INT") { connection.close { EventMachine.stop } } t.join amqp-1.8.0/examples/patterns/event/0000755000004100000410000000000013321132064017300 5ustar www-datawww-dataamqp-1.8.0/examples/patterns/event/consumer.rb0000644000004100000410000000234313321132064021462 0ustar www-datawww-data# encoding: utf-8 $LOAD_PATH.unshift File.expand_path("../../../../lib", __FILE__) require "amqp" require "yaml" t = Thread.new { EventMachine.run } sleep(0.5) connection = AMQP.connect channel = AMQP::Channel.new(connection, :auto_recovery => true) channel.on_error do |ch, channel_close| raise "Channel-level exception: #{channel_close.reply_text}" end channel.prefetch(1) channel.queue("", :durable => false, :auto_delete => true).bind("amqpgem.patterns.events").subscribe do |metadata, payload| begin body = YAML.load(payload) case metadata.type when "widgets.created" then puts "A widget #{body[:id]} was created" when "widgets.destroyed" then puts "A widget #{body[:id]} was destroyed" when "files.created" then puts "A new file (#{body[:filename]}, #{body[:sha1]}) was uploaded" when "files.indexed" then puts "A new file (#{body[:filename]}, #{body[:sha1]}) was indexed" else puts "[warn] Do not know how to handle event of type #{metadata.type}" end rescue Exception => e puts "[error] Could not handle event of type #{metadata.type}: #{e.inspect}" end end puts "[boot] Ready" Signal.trap("INT") { connection.close { EventMachine.stop } } t.join amqp-1.8.0/examples/patterns/event/producer.rb0000644000004100000410000000272513321132064021456 0ustar www-datawww-data# encoding: utf-8 $LOAD_PATH.unshift File.expand_path("../../../../lib", __FILE__) require "amqp" require "yaml" t = Thread.new { EventMachine.run } sleep(0.5) connection = AMQP.connect channel = AMQP::Channel.new(connection) exchange = channel.fanout("amqpgem.patterns.events", :durable => true, :auto_delete => false) EVENTS = { "pages.show" => { :url => "https://mysite.local/widgets/81772", :referrer => "http://www.google.com/search?client=safari&rls=en&q=widgets&ie=UTF-8&oe=UTF-8" }, "widgets.created" => { :id => 10, :shape => "round", :owner_id => 1000 }, "widgets.destroyed" => { :id => 10, :person_id => 1000 }, "files.created" => { :sha1 => "1a62429f47bc8b405d17e84b648f2fbebc555ee5", :filename => "document.pdf" }, "files.indexed" => { :sha1 => "1a62429f47bc8b405d17e84b648f2fbebc555ee5", :filename => "document.pdf", :runtime => 1.7623, :shared => "shard02" } } def generate_event n = (EVENTS.size * Kernel.rand).floor type = EVENTS.keys[n] payload = EVENTS[type] [type, payload] end # broadcast events EventMachine.add_periodic_timer(2.0) do event_type, payload = generate_event puts "Publishing a new event of type #{event_type}" exchange.publish(payload.to_yaml, :type => event_type) end puts "[boot] Ready. Will be publishing events every few seconds." Signal.trap("INT") { connection.close { EventMachine.stop } } t.join amqp-1.8.0/examples/patterns/request_reply/0000755000004100000410000000000013321132064021062 5ustar www-datawww-dataamqp-1.8.0/examples/patterns/request_reply/client.rb0000644000004100000410000000161313321132064022666 0ustar www-datawww-data# encoding: utf-8 $LOAD_PATH.unshift File.expand_path("../../../../lib", __FILE__) require "amqp" EventMachine.run do connection = AMQP.connect channel = AMQP::Channel.new(connection) replies_queue = channel.queue("", :exclusive => true, :auto_delete => true) replies_queue.subscribe do |metadata, payload| puts "[response] Response for #{metadata.correlation_id}: #{payload.inspect}" end # request time from a peer every 3 seconds EventMachine.add_periodic_timer(3.0) do puts "[request] Sending a request..." channel.default_exchange.publish("get.time", :routing_key => "amqpgem.examples.services.time", :message_id => Kernel.rand(10101010).to_s, :reply_to => replies_queue.name) end Signal.trap("INT") { connection.close { EventMachine.stop } } end amqp-1.8.0/examples/patterns/request_reply/server.rb0000644000004100000410000000145013321132064022715 0ustar www-datawww-data# encoding: utf-8 $LOAD_PATH.unshift File.expand_path("../../../../lib", __FILE__) require "amqp" EventMachine.run do connection = AMQP.connect channel = AMQP::Channel.new(connection) requests_queue = channel.queue("amqpgem.examples.services.time", :exclusive => true, :auto_delete => true) requests_queue.subscribe(:ack => true) do |metadata, payload| puts "[requests] Got a request #{metadata.message_id}. Sending a reply..." channel.default_exchange.publish(Time.now.to_s, :routing_key => metadata.reply_to, :correlation_id => metadata.message_id, :mandatory => true) metadata.ack end Signal.trap("INT") { connection.close { EventMachine.stop } } end amqp-1.8.0/examples/tls_certificates/0000755000004100000410000000000013321132064017646 5ustar www-datawww-dataamqp-1.8.0/examples/tls_certificates/client/0000755000004100000410000000000013321132064021124 5ustar www-datawww-dataamqp-1.8.0/examples/tls_certificates/client/req.pem0000644000004100000410000000162313321132064022420 0ustar www-datawww-data-----BEGIN CERTIFICATE REQUEST----- MIICbDCCAVQCAQAwJzEUMBIGA1UEAxMLZ2lvdmUubG9jYWwxDzANBgNVBAoTBmNs aWVudDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL/gJKoGOmY1egO3 nFTQPg40bdtBcFXhsmJ3+ebMi9Xrulu/9WmorEz5SqhX13T278f5/+MAq7Sh/V7V 37WjT0m2MmyFfixhESHb+fS6YQ0fXELK9ac/R4VOK2GP2qZVwrlQ6+hkpIphPiC0 IiliFFRBmQSE9gqG/F4/O2NEh5SCGQpBz1CLT94BGbx3dl2jQ5Gec00g/4bsGN5y Ym9MsI/88EMgYbqvV7Js166U1Z12AuBxSrQAIV6nEIlbpF0ZWsxeFaiZJy8qywhQ wB0N/Az19+rrgxgTZksbU4EE9580jjvDPyH1qTXlPZmkoVT1uJcuv9KcaF2E1HfV 3XqkC0sCAwEAAaAAMA0GCSqGSIb3DQEBBQUAA4IBAQAOpwkzMe1xYgYz8GOp5xF7 xJtfeh5z9JTt8ogvjS34mri2vNUPQd35H3y6Dhh9ZjLqC03mMV/KOtRCf7oJXpgZ S8C/NyJWNXB7/QNf9/EWEyOxoWYuvhTxqc0MhLPWrJRjgBKvvCGszBj2kWxDn7OO //TRq4ao2wQWHIlaP5htsr+rD4IJZqZC8lgptv0u9O/O8vruPUg3k6dnuiKdUyTL 2CDsiKSc6vZKJKL0TguBHSQj/sybAw1lyxIlpxiDcTCbM5FsER6jFydLly+ov3+x sqMOTieWU3qnaqOwK2ENqQfRMAk3FXEuDzvW4t7oxHB2gJfUJXfEVUWv7Kol25VT -----END CERTIFICATE REQUEST----- amqp-1.8.0/examples/tls_certificates/client/cert.pem0000644000004100000410000000204613321132064022566 0ustar www-datawww-data-----BEGIN CERTIFICATE----- MIIC5DCCAcygAwIBAgIBAjANBgkqhkiG9w0BAQUFADATMREwDwYDVQQDEwhNeVRl c3RDQTAeFw0xMTA0MTcyMDE4MzNaFw0xMjA0MTYyMDE4MzNaMCcxFDASBgNVBAMT C2dpb3ZlLmxvY2FsMQ8wDQYDVQQKEwZjbGllbnQwggEiMA0GCSqGSIb3DQEBAQUA A4IBDwAwggEKAoIBAQC/4CSqBjpmNXoDt5xU0D4ONG3bQXBV4bJid/nmzIvV67pb v/VpqKxM+UqoV9d09u/H+f/jAKu0of1e1d+1o09JtjJshX4sYREh2/n0umENH1xC yvWnP0eFTithj9qmVcK5UOvoZKSKYT4gtCIpYhRUQZkEhPYKhvxePztjRIeUghkK Qc9Qi0/eARm8d3Zdo0ORnnNNIP+G7BjecmJvTLCP/PBDIGG6r1eybNeulNWddgLg cUq0ACFepxCJW6RdGVrMXhWomScvKssIUMAdDfwM9ffq64MYE2ZLG1OBBPefNI47 wz8h9ak15T2ZpKFU9biXLr/SnGhdhNR31d16pAtLAgMBAAGjLzAtMAkGA1UdEwQC MAAwCwYDVR0PBAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMCMA0GCSqGSIb3DQEB BQUAA4IBAQCVBpz3gZRr1s48SVF4+C9YLzrSaWsvzKZNDKH7RJ4f1VR//ZY5zsYi RqSlzSfLM76Y6Y/Eq0iFshtKmuXHKyA4r/Gp+iiCw4U9Htk91rg98wAPc8wOBgn1 OmomH65JpLwxYvUwyt91opGppcqZHWhruhI0fFTFtPIlGKK3KOmJLPpaSvY0YTJ+ vaI3D6yQEMQoZ/mcXk928ofJJvOpUEmvjTW4Orz+T8NmiffLb64P50h86bdV+8tw FJx6ix6vLF41LU2iPEYHuuXkA7+M5e+POGscJJCb1p6JKxzI6D/UVDnrbhOlqBa5 U45f0oXQ/ndOYUrBRu3BPFdNAjvpa0ld -----END CERTIFICATE----- amqp-1.8.0/examples/tls_certificates/client/keycert.p120000644000004100000410000000445513321132064023126 0ustar www-datawww-data0 )0 *H 00 *H 0|0u *H 0 *H  0o;*8THJ tʄ?ɷ`qo51=ܮM[l]Ճ% i:8yf*~lGj8?r9]D.tՙ^bN"2;\7HOiɖ+܏'rŗ,A7㹿}[-v~ʻ#1VGu|TOꮔx)ӥ lj⇛aW?->pȰDj5uɧbkRH]8Xڲ_/}73'#Ǭr}ŋ8Vtj'k/e&2sK.&+# 1VdwpV鹀UU ``ﲭW=Ћu%8?n"*L3Hݒ';ThqvWcm|g/NpSEƔn{nXσw+ ϑ@K^j]~K ,"Nא[fҀqժ5 &HզRxNO>=l VT0<` -k, rŮ _MT<\l6ۀ$8y`krua,3Yj)D?sNayȔ!Oqt,>XAEI9A'0޽|?1ʛ +/; wE]ˍ5KFzQO.v7&Zk?_G%0A *H 2.0*0& *H  00 *H  05ȫB#CӬ$J\)٥C[ʻ-I ~j "'{ #'`IԻ/; ; `e]-, 3N{ۄUF.1ć^ĎJAעqߺ0UO/a☏ZE@e - /s:Xze\Q7Tu+/aEޭW7:=b/>ւPׅo94IXO cȽJ79SSȿGewN0m ThY03b,sNW*YI1Arҝ!mƃB퍟;!`8I$áO@5iFdyANS}-"c}& ufyigaE˺:2oMO-c~&bjS}dkNN[3%:;KLD% 4._b,ɇfs䞾!jHꓵ]-{jiu%Ja@LTi*?(ʞ7P LC" [ךsRX(U8w@tRJ)UG+ HQژ )9LvAH/ ټ"c'[scन]&E l%rRٱVVnGƆroRImsFY# ZY"8.oq 5"`%\3 m^zY"\[/Boلq %>4(ɒnSl]6=˰p}wRZcf+\_ 7@cV4FdOE6\Dn!{%4Tha 9qX<`!YFGe.i*1\$dڄh t&O VtL?Z:oK$1Y,5 <hMF[?H|#/KU2C&z)ofPB#~YF,T!#\({s2`̙!忌zAql";+_8aˢw[Q[ g_ecu9b IrӚ ﶠxpLk٢=n>G7{ZL,|QOlfAO^gn^zrF7Aq SBpQ ] f#hJ2rB4&A CZ>ϒc\~^0,@*cc?BRB6`֔v[VK'Nk&N4zLZ ԧ5rBr̽S \#Zelf]ИXkQ߁M89dg4b|j0q kME g&E=.6QB`e#9*Θ LIgD`4񽕫*Md|K]2Ƒ4w`䶝~K;BjljELVGN .٨&p#v>[{ I.dء6|))] kd)YS,qX"1 ߜL %BO/)dh]WBqX0A *H 2.0*0& *H  00 *H  0K qN&tt0ՓO)b8C. n}ZϿ`SWJ5>SGakw# zʮM9FSi%UlL"r–nRv*߿( P:pO ~:$ :M2JCˏNlWf:0Cfes9.O*m|1DDRd*1V?Z{W/LQQ8Z釴 ɀvTJ^۸A'%J|niGS +aڱJ|) h~j+&(|XG [iWʹc-FNDZɇه8GP[pCd +K5n]*COĢΥ~ПK@|y_ ۔":Ciu ~QiCPm>D8QxjTwyCpE,G8?~.o$o ,es"Ů܈y|mL! QtCRRiΞ%s!a^-csxZ>e=ێz!>G18mQ-+>ue5*fQ_uy0(fx+hzʞ_ERfpʴNT{ ^`8 TZ!qOa\⼰?hc~"B􆆘8O('āvVW5ŭ6ƉhVXö{q!z8[3("_Sa`42Eub5EBiѫy>*۾~eHp[-bM! |/x"DՁn/R1%0# *H  1M2~Z\1c010!0 +oOY7".vݟ; 8amqp-1.8.0/examples/tls_certificates/server/key.pem0000644000004100000410000000321713321132064022452 0ustar www-datawww-data-----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEA3QRKZ5fgUXCM2eDOqQeuPXE/2FMXm03pFKtB4RVwE22uOF5E sfNTZ+XTFZyoCYCVUYQcVNGaQbeP8KICmmmCOPzgBIG1HB9NzpxFxzSwefOniRnW hR5EkDn51GwSM6UCpUIxi345p+pl5C1uZSz+Tqzfp6bhCOFqv44GyEEKfbeysIPj 2/VXSRsNHZjJVXzz/BVQkaRgfZSXNQMsPICh4OJov7siihXS1fSFn/g15b0UNr5Y 5kBIUquG78ZwrZuKeQewBR3n055XUoBkVGbsiOf+LxTMqQd3XlfpZLveedeLjRdI IOungwH6DYxJnGtPT/hgcvC3ybLogXhVeIq7YwIDAQABAoIBAEu0+YussZET/Ztw bznlQKEZVuZR6Ccxs+J5m1JvlnmBO4lheSR/lhVj2z9u7vx6SCupFk9TkQRrzWl/ BWdBNvMwY8gHajNc4bkhPKG1AbJ0bPIAoMPuj0vcICDMeBuqrVJQb0o6DaPgHdDg Yw1TMTVf8CiseC8jj/5XtykHZoGTNTKzusvTjL8hTXFaWQfHQaN3WC1qwrFU2+x6 AJeoSz5F1Q/lykqVAdl2B1L39kiSCAkbVE1GI2qjftCff3ycufYV/fxXeyZwZx9B NGWUJFyZte8EcrAUoo9B/gvALGDbJsSUsbri+HsRsdOQT3K/giafUipX2FB8Bnxm nasEfskCgYEA74PrKYo13mTUWRJ2FQTBRHLsMR53PK83RpRRs8361/uCJrjpqfdD 2fUt8kH0rmm2vaWYsllucJoYdYSC9CQdECLzpOZS907PJHXCOsFsBjw74fvjZ+gY 9EXIENZSOSR1PQCWZm+5lL4xi/T+beWBfz0YksErj2GM7dyJeQjkIz8CgYEA7Dpx ANgrgO9WTu51NIlKt3P7+t6q6MHQcWuzkWMBmVU4zxeyggux+ucMj+l3KLyw7fLT jRz03eGpqjQT8Yl676uYONhTDC9VOBbLgjShuVOX7lleMLxVFJWqL1FphNghxysF HVCq1WH0Qu4keirPEotBxkNZRaRYHwRYlVPKMt0CgYEApIHSAj0IlNBiPS995R/X 8sCQU4heU1LxP0vd9gZy1OfNU/VLoE7RzqEkxrDgcu7u8cEMaOsd/L8KL6UtIKyx PYUUHV2I/I2nnp43Io35OSsj4ipU3eg/Q3+uU0oxPUg6MgT2SDNSnsQnWb6TBj5N PGxlNV7yIU/aMQF5dqVRtJcCgYEArC4Mn6jwTJImPnHgS+Kl6wFG8JvLxss9uu3d fGLFj5VmSsvi+Ja9qzstFNf+WlruOwF64KfycqdAmyZKQwsJ6BcSZJyIK6F0Y+V5 f/YMyp/7ZWcOGEetW8uat9KHLqS6OglJOQzK96zl9MLPI5yAQevujSwZrYEUGcd5 KZ5hCqECgYBExYSDJOuSMvKFw66jrLiBedLPuzh1iznwd6e4idpqIUkdhbEuXhHG +35HeN5cGwjbjXtIjxJsAddTbjvSqaPukmBKLm1ABqF3r0irbNaPcK1xfG9a5L28 /enwipaSWjGovfDXWqmmCvWC8M7iGiqFChI87WlzbvaAapOW0D576Q== -----END RSA PRIVATE KEY----- amqp-1.8.0/examples/tls_certificates/testca/0000755000004100000410000000000013321132064021131 5ustar www-datawww-dataamqp-1.8.0/examples/tls_certificates/testca/serial.old0000644000004100000410000000000313321132064023101 0ustar www-datawww-data02 amqp-1.8.0/examples/tls_certificates/testca/serial0000644000004100000410000000000313321132064022324 0ustar www-datawww-data03 amqp-1.8.0/examples/tls_certificates/testca/index.txt.attr0000644000004100000410000000002513321132064023747 0ustar www-datawww-dataunique_subject = yes amqp-1.8.0/examples/tls_certificates/testca/certs/0000755000004100000410000000000013321132064022251 5ustar www-datawww-dataamqp-1.8.0/examples/tls_certificates/testca/certs/01.pem0000644000004100000410000000204613321132064023176 0ustar www-datawww-data-----BEGIN CERTIFICATE----- MIIC5DCCAcygAwIBAgIBATANBgkqhkiG9w0BAQUFADATMREwDwYDVQQDEwhNeVRl c3RDQTAeFw0xMTA0MTcyMDE3MjdaFw0xMjA0MTYyMDE3MjdaMCcxFDASBgNVBAMT C2dpb3ZlLmxvY2FsMQ8wDQYDVQQKEwZzZXJ2ZXIwggEiMA0GCSqGSIb3DQEBAQUA A4IBDwAwggEKAoIBAQDdBEpnl+BRcIzZ4M6pB649cT/YUxebTekUq0HhFXATba44 XkSx81Nn5dMVnKgJgJVRhBxU0ZpBt4/wogKaaYI4/OAEgbUcH03OnEXHNLB586eJ GdaFHkSQOfnUbBIzpQKlQjGLfjmn6mXkLW5lLP5OrN+npuEI4Wq/jgbIQQp9t7Kw g+Pb9VdJGw0dmMlVfPP8FVCRpGB9lJc1Ayw8gKHg4mi/uyKKFdLV9IWf+DXlvRQ2 vljmQEhSq4bvxnCtm4p5B7AFHefTnldSgGRUZuyI5/4vFMypB3deV+lku95514uN F0gg66eDAfoNjEmca09P+GBy8LfJsuiBeFV4irtjAgMBAAGjLzAtMAkGA1UdEwQC MAAwCwYDVR0PBAQDAgUgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMA0GCSqGSIb3DQEB BQUAA4IBAQCCmvsAQ1GaPfoKMqCWxyrQ2BNVPWm9J65thZKxZQF01gLox9cpv6+X QERcD71HStxbzh2B8Hebbk9OplGuyrdgfvzUxcn88kObj6udipDx4YxTJvtff/9w xeD5OWDVgef0GkB1Rjj3C3W/sfmTZBYfdKuWuwxMG/NxISkQP4aFHwJnPrzNx9ON bHoKVNrQ2iKAiMysjnFeA/4QuhBQRf41h9SBWwJEW3Ts91TzbgcjCL46Dq29QB9A 4v8t6K/nibP6n53zHbVzdxIEJ2hFKm+vuqaHRW3048Xgww1xkdxrVbyGp9si92i1 KJ5SDIOR8bQ7OXvdvpiyy6p9fIG0Z6w0 -----END CERTIFICATE----- amqp-1.8.0/examples/tls_certificates/testca/certs/02.pem0000644000004100000410000000204613321132064023177 0ustar www-datawww-data-----BEGIN CERTIFICATE----- MIIC5DCCAcygAwIBAgIBAjANBgkqhkiG9w0BAQUFADATMREwDwYDVQQDEwhNeVRl c3RDQTAeFw0xMTA0MTcyMDE4MzNaFw0xMjA0MTYyMDE4MzNaMCcxFDASBgNVBAMT C2dpb3ZlLmxvY2FsMQ8wDQYDVQQKEwZjbGllbnQwggEiMA0GCSqGSIb3DQEBAQUA A4IBDwAwggEKAoIBAQC/4CSqBjpmNXoDt5xU0D4ONG3bQXBV4bJid/nmzIvV67pb v/VpqKxM+UqoV9d09u/H+f/jAKu0of1e1d+1o09JtjJshX4sYREh2/n0umENH1xC yvWnP0eFTithj9qmVcK5UOvoZKSKYT4gtCIpYhRUQZkEhPYKhvxePztjRIeUghkK Qc9Qi0/eARm8d3Zdo0ORnnNNIP+G7BjecmJvTLCP/PBDIGG6r1eybNeulNWddgLg cUq0ACFepxCJW6RdGVrMXhWomScvKssIUMAdDfwM9ffq64MYE2ZLG1OBBPefNI47 wz8h9ak15T2ZpKFU9biXLr/SnGhdhNR31d16pAtLAgMBAAGjLzAtMAkGA1UdEwQC MAAwCwYDVR0PBAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMCMA0GCSqGSIb3DQEB BQUAA4IBAQCVBpz3gZRr1s48SVF4+C9YLzrSaWsvzKZNDKH7RJ4f1VR//ZY5zsYi RqSlzSfLM76Y6Y/Eq0iFshtKmuXHKyA4r/Gp+iiCw4U9Htk91rg98wAPc8wOBgn1 OmomH65JpLwxYvUwyt91opGppcqZHWhruhI0fFTFtPIlGKK3KOmJLPpaSvY0YTJ+ vaI3D6yQEMQoZ/mcXk928ofJJvOpUEmvjTW4Orz+T8NmiffLb64P50h86bdV+8tw FJx6ix6vLF41LU2iPEYHuuXkA7+M5e+POGscJJCb1p6JKxzI6D/UVDnrbhOlqBa5 U45f0oXQ/ndOYUrBRu3BPFdNAjvpa0ld -----END CERTIFICATE----- amqp-1.8.0/examples/tls_certificates/testca/index.txt.old0000644000004100000410000000006513321132064023557 0ustar www-datawww-dataV 120416201727Z 01 unknown /CN=giove.local/O=server amqp-1.8.0/examples/tls_certificates/testca/index.txt0000644000004100000410000000015213321132064022777 0ustar www-datawww-dataV 120416201727Z 01 unknown /CN=giove.local/O=server V 120416201833Z 02 unknown /CN=giove.local/O=client amqp-1.8.0/examples/tls_certificates/testca/cacert.pem0000644000004100000410000000177513321132064023107 0ustar www-datawww-data-----BEGIN CERTIFICATE----- MIICxjCCAa6gAwIBAgIJAOF/re9e0JhgMA0GCSqGSIb3DQEBBQUAMBMxETAPBgNV BAMTCE15VGVzdENBMB4XDTExMDQxNzIwMDcxN1oXDTEyMDQxNjIwMDcxN1owEzER MA8GA1UEAxMITXlUZXN0Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB AQDK8ZdVo1llXXrS7ed5Luel1VRq0DmJG1TZ8bAgovK9fx6WsgzeJ17zYwAEheUz B8PogkNqU3ZIa4T51VCtamoiG6bbKYpco4lutKM7aNGNJNcUfDhwyt/NYOxXM3xf ahMWjrbH0e4qKJgEjnJLpoeEa+YquDG2NXzocZY5+upy0pX6Reh1EQbju69j9f5Q Z6u4cvraScyN5IYuq3lKTmc2TxVyINVkpK9DlGJZUBAOBLSbFxGZDRoY0D3b1aeS /XMfVvcrJi48Ns2ZvT+CAy0KVeKBeQ9+6kD/YAizcxDGLRvqOZXOHYaE2L51DabL QTxk9NFPgIxrYtGS7RtQ7dUhAgMBAAGjHTAbMAwGA1UdEwQFMAMBAf8wCwYDVR0P BAQDAgEGMA0GCSqGSIb3DQEBBQUAA4IBAQAzFtKwsrCBaIAf1/VCJZh4+gzs/UW+ UXeC17GRzazb0O0drm9aMOtwE48NGVTwBjqmnPzFg56FYyll3oVZHz/fgkQZ+6iu qAbwlmY2H+L9RyC9Mv3B+Ew/o3MNCQQ3rgxbA7hb0k8mwDplxS5GBRROwZl/zHks gb4yPR6G83+zrgn6JIybdEGUySc1XDx+p6fVXvQG/1fsmExN9/ZuC6ulgBF7e+R5 d7l1AluGL4kS7GMDRZnU9QcXkhnlUyPXIDr/Jd1HFKtwgrXtVl5YIWTaRdWOXGwX Q8BpM3Vk/NQFoTHO4Na3y8JY6iJzYTIXWHjI6RJdUffsEPtBoysHFHYv -----END CERTIFICATE----- amqp-1.8.0/examples/tls_certificates/testca/openssl.cnf0000644000004100000410000000500613321132064023305 0ustar www-datawww-data[ ca ] default_ca = testca [ testca ] dir = . certificate = $dir/cacert.pem database = $dir/index.txt new_certs_dir = $dir/certs private_key = $dir/private/cakey.pem serial = $dir/serial default_crl_days = 7 default_days = 365 default_md = sha1 policy = testca_policy x509_extensions = certificate_extensions [ testca_policy ] commonName = supplied stateOrProvinceName = optional countryName = optional emailAddress = optional organizationName = optional organizationalUnitName = optional [ certificate_extensions ] basicConstraints = CA:false [ req ] default_bits = 2048 default_keyfile = ./private/cakey.pem default_md = sha1 prompt = yes distinguished_name = root_ca_distinguished_name x509_extensions = root_ca_extensions [ root_ca_distinguished_name ] commonName = hostname [ root_ca_extensions ] basicConstraints = CA:true keyUsage = keyCertSign, cRLSign [ client_ca_extensions ] basicConstraints = CA:false keyUsage = digitalSignature extendedKeyUsage = 1.3.6.1.5.5.7.3.2 [ server_ca_extensions ] basicConstraints = CA:false keyUsage = keyEncipherment extendedKeyUsage = 1.3.6.1.5.5.7.3.1amqp-1.8.0/examples/tls_certificates/testca/private/0000755000004100000410000000000013321132064022603 5ustar www-datawww-dataamqp-1.8.0/examples/tls_certificates/testca/private/cakey.pem0000644000004100000410000000321313321132064024401 0ustar www-datawww-data-----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEAyvGXVaNZZV160u3neS7npdVUatA5iRtU2fGwIKLyvX8elrIM 3ide82MABIXlMwfD6IJDalN2SGuE+dVQrWpqIhum2ymKXKOJbrSjO2jRjSTXFHw4 cMrfzWDsVzN8X2oTFo62x9HuKiiYBI5yS6aHhGvmKrgxtjV86HGWOfrqctKV+kXo dREG47uvY/X+UGeruHL62knMjeSGLqt5Sk5nNk8VciDVZKSvQ5RiWVAQDgS0mxcR mQ0aGNA929Wnkv1zH1b3KyYuPDbNmb0/ggMtClXigXkPfupA/2AIs3MQxi0b6jmV zh2GhNi+dQ2my0E8ZPTRT4CMa2LRku0bUO3VIQIDAQABAoIBAQDJoXiDHFVgUZ0L XlUBYKnEaIyDxzey0hXep7Me6eaUgW0JugLw4VsEI9NLqyBKMCfjpTCHvj6huzmV 4utSMI0cMC76Rm5ylgSgmhYnm3+/ZN/QOY71+YqcCfUmuj+SqNgoLEjLhPbEqipH NKO4J88ysOUwgmrZppDgfKIOHw66Xlx0WtyFksozLve5pqxzs2gNZDmT5YdlGt+w X2zaUr7GholPGUVzhZSlpBpPkloSNYyGPX7O25bc63Ev92m3vnroJZYFiaUaKcFE M2RVVd3m8J47uoSwxl2ppnIgwodTWe20swc3d4cXK30U4USLsvWesthnfUc65QWa KfeanOrBAoGBAOvxGM9ufV1EFyV3D4kDB2UGp2ccv2t+rJ+urpl7JLCw1Aetpc3v Qg/QsbBfOhfI650zOZgzE5bG6B6wNYVLAC/UusrYgeLkyqKVbQgDTP+djD61jYjy IG5RP7EumN07Gvja59B4Kw5zN6TB3MKGz9Qv3a2MOvyg/3TRnssHOkQ5AoGBANwy V822s149G46tIbESw5ICcY8HorP1kD2syMeBQWKJcEgiSXL8Q3TIKdqaim8Q1Bqu fSYwISUVsFDaTyvRl25hLfkQ0t+qsul7EcjNFHf9L7dJx7z/dj8lVTQWGYse5iVQ aplx7fYQHgXdC7NjtpIrxUZkJ7bAl+0cpavdcCgpAoGAetCbO46mDyBcdBIPsiAz fzEBfrkGIyxjKxPAqv/gz2CcXgrT3eiHGLhnZgmLscnSa5e4iTM9JSUQuri6g1HR HRS8zs34fmTd3deuU5d0QzJ9SD81F24B16rPXqmExNP5bER2mpuSvgjXlBmdklye XjM0TxxJsCsWDnb3E3QFrnECgYBSpXqbNZXBK0JqnMTmh1psNQqWWpFQ5jxLScza RMNbzqYcDPJwfAp9jJtY92Q6J6DUmuVSLgJivu88iZPpqHMj9MmikBP160XXqF+W dJLYLml4a/LSFzg0nziJojnYI7LSEorQKRjdoFMEdGDt5eEin9cdgn39c/ASCQyN o0FzcQKBgHifRoEEyyHzepXiv2osPsOV9cEoWROLRaooE7boPISimJ1PCFGON0XT 20PmWJL3j3/f2Eqk47x4WpzLOp/OwUqqpaDpImtQX7GD7C+/PbAdbT1Q1Kc9kxc9 a5FJg85oJqDPB1yEmYG5nWIlqB3LOX/IdOfYSOUFvRiomwJJoxys -----END RSA PRIVATE KEY----- amqp-1.8.0/examples/tls_certificates/testca/index.txt.attr.old0000644000004100000410000000002513321132064024524 0ustar www-datawww-dataunique_subject = yes amqp-1.8.0/examples/tls_certificates/testca/cacert.cer0000644000004100000410000000131213321132064023062 0ustar www-datawww-data00 ^И`0  *H 010UMyTestCA0 110417200717Z 120416200717Z010UMyTestCA0"0  *H 0 UYe]zy.Tj9T  '^c3CjSvHkPjj")\n;hэ$|8p`W3|_j*(rKk*15|q9rҕEu㻯cPgrI̍.yJNg6Or dCbYP =էsV+&.<6͙?- Uy~@`s-9ؾu A 5671, :ssl => { :cert_chain_file => certificate_chain_file_path, :private_key_file => client_private_key_file_path }) do |connection| puts "Connected, authenticated. TLS seems to work." connection.disconnect { puts "Now closing the connection…"; EventMachine.stop } end amqp-1.8.0/examples/rack/0000755000004100000410000000000013321132064015237 5ustar www-datawww-dataamqp-1.8.0/examples/rack/publish_a_message_on_request/0000755000004100000410000000000013321132064023155 5ustar www-datawww-dataamqp-1.8.0/examples/rack/publish_a_message_on_request/thin.ru0000644000004100000410000000151513321132064024471 0ustar www-datawww-datause Rack::CommonLogger require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../../lib", __FILE__)) require 'amqp' require 'amqp/utilities/event_loop_helper' puts "EventMachine.reactor_running? => #{EventMachine.reactor_running?.inspect}" AMQP::Utilities::EventLoopHelper.run do AMQP.start exchange = AMQP.channel.fanout("amq.fanout") q = AMQP.channel.queue("", :auto_delete => true, :exclusive => true) q.bind(exchange) AMQP::channel.default_exchange.publish("Started!", :routing_key => q.name) end app = proc do |env| AMQP.channel.fanout("amq.fanout").publish("Served a request at (#{Time.now.to_i})") [ 200, # Status code { # Response headers 'Content-Type' => 'text/html', 'Content-Length' => '2', }, ['hi'] # Response body ] end run appamqp-1.8.0/examples/routing/0000755000004100000410000000000013321132064016006 5ustar www-datawww-dataamqp-1.8.0/examples/routing/fanout_routing.rb0000644000004100000410000000160513321132064021400 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require "amqp" EventMachine.run do AMQP.connect("amqp://dev.rabbitmq.com") do |connection| channel = AMQP::Channel.new(connection) exchange = channel.topic("amqpgem.examples.routing.fanout_routing", :auto_delete => true) # Subscribers. 10.times do q = channel.queue("", :exclusive => true, :auto_delete => true).bind(exchange) q.subscribe do |payload| puts "Queue #{q.name} received #{payload}" end end # Publish some test data in a bit, after all queues are declared & bound EventMachine.add_timer(1.2) { exchange.publish "Hello, fanout exchanges world!" } show_stopper = Proc.new { connection.close { EventMachine.stop } } Signal.trap "TERM", show_stopper EM.add_timer(3, show_stopper) end end amqp-1.8.0/examples/routing/round_robin_with_the_default_exchange.rb0000644000004100000410000000211013321132064026106 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require "amqp" EventMachine.run do AMQP.connect do |connection| channel1 = AMQP::Channel.new(connection) channel2 = AMQP::Channel.new(connection) exchange = channel1.default_exchange q1 = channel1.queue("amqpgem.examples.queues.shared", :auto_delete => true) q1.subscribe do |payload| puts "Queue #{q1.name} on channel 1 received #{payload}" end q2 = channel2.queue("amqpgem.examples.queues.shared", :auto_delete => true) q2.subscribe do |payload| puts "Queue #{q2.name} on channel 2 received #{payload}" end # Publish some test data in a bit, after all queues are declared & bound EventMachine.add_timer(1.2) do 5.times { |i| exchange.publish("Hello #{i}, direct exchanges world!", :routing_key => "amqpgem.examples.queues.shared") } end show_stopper = Proc.new { connection.close { EventMachine.stop } } Signal.trap "TERM", show_stopper EM.add_timer(3, show_stopper) end end amqp-1.8.0/examples/routing/headers_routing.rb0000644000004100000410000000440713321132064021522 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' puts "=> Headers routing example" puts AMQP.start do |connection| channel = AMQP::Channel.new(connection) channel.on_error do |ch, channel_close| puts "A channel-level exception: #{channel_close.inspect}" end exchange = channel.headers("amq.match", :durable => true) channel.queue("", :auto_delete => true).bind(exchange, :arguments => { 'x-match' => 'all', :arch => "ia64", :os => 'linux' }).subscribe do |metadata, payload| puts "[linux/ia64] Got a message: #{payload}" end channel.queue("", :auto_delete => true).bind(exchange, :arguments => { 'x-match' => 'all', :arch => "x86", :os => 'linux' }).subscribe do |metadata, payload| puts "[linux/x86] Got a message: #{payload}" end channel.queue("", :auto_delete => true).bind(exchange, :arguments => { :os => 'linux'}).subscribe do |metadata, payload| puts "[linux] Got a message: #{payload}" end channel.queue("", :auto_delete => true).bind(exchange, :arguments => { 'x-match' => 'any', :os => 'macosx', :cores => 8 }).subscribe do |metadata, payload| puts "[macosx|octocore] Got a message: #{payload}" end channel.queue("", :auto_delete => true).bind(exchange, :arguments => { :package => { :name => 'riak', :version => '0.14.2' } }).subscribe do |metadata, payload| puts "[riak/0.14.2] Got a message: #{payload}" end EventMachine.add_timer(0.5) do exchange.publish "For linux/ia64", :headers => { :arch => "ia64", :os => 'linux' } exchange.publish "For linux/x86", :headers => { :arch => "x86", :os => 'linux' } exchange.publish "For linux", :headers => { :os => 'linux' } exchange.publish "For OS X", :headers => { :os => 'macosx' } exchange.publish "For solaris/ia64", :headers => { :os => 'solaris', :arch => 'ia64' } exchange.publish "For ocotocore", :headers => { :cores => 8 } exchange.publish "For nodes with Riak 0.14.2", :headers => { :package => { :name => 'riak', :version => '0.14.2' } } end show_stopper = Proc.new do $stdout.puts "Stopping..." connection.close { EventMachine.stop } end Signal.trap "INT", show_stopper EventMachine.add_timer(2, show_stopper) end amqp-1.8.0/examples/routing/round_robin_with_direct_exchange.rb0000644000004100000410000000244413321132064025106 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require "amqp" EventMachine.run do AMQP.connect do |connection| channel1 = AMQP::Channel.new(connection) channel2 = AMQP::Channel.new(connection) exchange = channel1.direct("amqpgem.examples.exchanges.direct", :auto_delete => true) q1 = channel1.queue("amqpgem.examples.queues.shared", :auto_delete => true).bind(exchange, :routing_key => "shared.key") q1.subscribe do |payload| puts "Queue #{q1.name} on channel 1 received #{payload}" end # since the queue is shared, binding here is redundant but we will leave it in for completeness. q2 = channel2.queue("amqpgem.examples.queues.shared", :auto_delete => true).bind(exchange, :routing_key => "shared.key") q2.subscribe do |payload| puts "Queue #{q2.name} on channel 2 received #{payload}" end # Publish some test data in a bit, after all queues are declared & bound EventMachine.add_timer(1.2) do 5.times { |i| exchange.publish("Hello #{i}, direct exchanges world!", :routing_key => "shared.key") } end show_stopper = Proc.new { connection.close { EventMachine.stop } } Signal.trap "TERM", show_stopper EM.add_timer(3, show_stopper) end end amqp-1.8.0/examples/routing/pubsub.rb0000755000004100000410000000232513321132064017640 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require "amqp" EventMachine.run do AMQP.connect do |connection| channel = AMQP::Channel.new(connection) exchange = channel.topic("pub/sub") # Subscribers. channel.queue("Everything about development").bind(exchange, :routing_key => "technology.dev.#").subscribe do |payload| puts "A new dev post: '#{payload}'" end channel.queue("Everything about rubies").bind(exchange, :routing_key => "#.ruby").subscribe do |headers, payload| puts "A new post about rubies: '#{payload}', routing key = #{headers.routing_key}" end # Let's publish some test data. exchange.publish "Ruby post", :routing_key => "technology.dev.ruby" exchange.publish "Erlang post", :routing_key => "technology.dev.erlang" exchange.publish "Sinatra post", :routing_key => "technology.web.ruby" exchange.publish "Jewelery post", :routing_key => "jewelery.ruby" show_stopper = Proc.new { connection.close do EM.stop end } Signal.trap "INT", show_stopper Signal.trap "TERM", show_stopper EM.add_timer(1, show_stopper) end end amqp-1.8.0/examples/routing/unroutable_mandatory_message_is_returned.rb0000644000004100000410000000170613321132064026704 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' puts "=> Handling a returned unroutable message that was published as mandatory" puts AMQP.start(:host => '127.0.0.1') do |connection| channel = AMQP.channel channel.on_error { |ch, channel_close| EventMachine.stop; raise "channel error: #{channel_close.reply_text}" } # this exchange has no bindings, so messages published to it cannot be routed. exchange = channel.fanout("amqpgem.examples.fanout", :auto_delete => true) exchange.on_return do |basic_return, metadata, payload| puts "#{payload} was returned! reply_code = #{basic_return.reply_code}, reply_text = #{basic_return.reply_text}" end EventMachine.add_timer(0.3) { 10.times do |i| exchange.publish("Message ##{i}", :mandatory => true) end } EventMachine.add_timer(2) { connection.close { EventMachine.stop } } end amqp-1.8.0/examples/routing/weather_updates.rb0000644000004100000410000000545113321132064021524 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require "amqp" EventMachine.run do AMQP.connect do |connection| channel = AMQP::Channel.new(connection) exchange = channel.topic("pub/sub", :auto_delete => true) # Subscribers. channel.queue("", :exclusive => true) do |queue| queue.bind(exchange, :routing_key => "americas.north.#").subscribe do |metadata, payload| puts "An update for North America: #{payload}, routing key is #{metadata.routing_key}" end end channel.queue("americas.south").bind(exchange, :routing_key => "americas.south.#").subscribe do |metadata, payload| puts "An update for South America: #{payload}, routing key is #{metadata.routing_key}" end channel.queue("us.california").bind(exchange, :routing_key => "americas.north.us.ca.*").subscribe do |metadata, payload| puts "An update for US/California: #{payload}, routing key is #{metadata.routing_key}" end channel.queue("us.tx.austin").bind(exchange, :routing_key => "#.tx.austin").subscribe do |metadata, payload| puts "An update for Austin, TX: #{payload}, routing key is #{metadata.routing_key}" end channel.queue("it.rome").bind(exchange, :routing_key => "europe.italy.rome").subscribe do |metadata, payload| puts "An update for Rome, Italy: #{payload}, routing key is #{metadata.routing_key}" end channel.queue("asia.hk").bind(exchange, :routing_key => "asia.southeast.hk.#").subscribe do |metadata, payload| puts "An update for Hong Kong: #{payload}, routing key is #{metadata.routing_key}" end # publish a bunch of messages after 1 second, when all queues are declared and bound EventMachine.add_timer(1) do exchange.publish("San Diego update", :routing_key => "americas.north.us.ca.sandiego"). publish("Berkeley update", :routing_key => "americas.north.us.ca.berkeley"). publish("San Francisco update", :routing_key => "americas.north.us.ca.sanfrancisco"). publish("New York update", :routing_key => "americas.north.us.ny.newyork"). publish("São Paolo update", :routing_key => "americas.south.brazil.saopaolo"). publish("Hong Kong update", :routing_key => "asia.southeast.hk.hongkong"). publish("Kyoto update", :routing_key => "asia.southeast.japan.kyoto"). publish("Shanghai update", :routing_key => "asia.southeast.prc.shanghai"). publish("Rome update", :routing_key => "europe.italy.roma"). publish("Paris update", :routing_key => "europe.france.paris") end show_stopper = Proc.new { connection.close { EventMachine.stop } } Signal.trap "TERM", show_stopper EM.add_timer(3, show_stopper) end end amqp-1.8.0/examples/issues/0000755000004100000410000000000013321132064015632 5ustar www-datawww-dataamqp-1.8.0/examples/issues/issue_121.rb0000644000004100000410000000117713321132064017700 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' require 'amqp/extensions/rabbitmq' AMQP.start(:user => 'guest',:pass => 'guest',:vhost => '/') do |connection| channel = AMQP::Channel.new(connection) channel.queue("shuki_q",{:nowait => true,:passive=>false, :auto_delete=>false, :arguments=>{"x-expires"=>5600000}}) do |queue| queue.bind('raw.shuki',:routing_key => 'ShukiesTukies') puts "before subscribe" queue.subscribe(:ack => true) do |header, payload| puts "inside subscribe" p header.class end end end amqp-1.8.0/examples/issues/issue_93.rb0000644000004100000410000000115713321132064017626 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "rubygems" require 'amqp' puts "Running amqp gem #{AMQP::VERSION}" AMQP.start("amqp://guest:guest@localhost:5672") do |connection, open_ok| ch = AMQP::Channel.new(connection) ch.direct("amqpgem.issues.93.1") ch.direct("amqpgem.issues.93.2", :auto_delete => false) ch.direct("amqpgem.issues.93.3") do |ex, declare_ok| end ch.direct("amqpgem.issues.93.4", :auto_delete => false) do |ex, declare_ok| end ch.direct("amqpgem.issues.93.5", :auto_delete => true) ch.direct("amqpgem.issues.93.6", :auto_delete => true) do |ex, declare_ok| end puts "Done" endamqp-1.8.0/examples/issues/issue_94.rb0000644000004100000410000000074313321132064017627 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 if defined?(Bundler) Bundler.setup else require "rubygems" end require "amqp" puts "Running amqp gem #{AMQP::VERSION}" AMQP.start(:host => '127.0.0.1') do |connection| channel = AMQP::Channel.new(connection) exchange = channel.direct("") queue = channel.queue("indexer_queue", { :durable => true }) EM.add_periodic_timer(1) { queue.status do |num_messages, num_consumers| puts "msgs:#{num_messages}" end } endamqp-1.8.0/examples/issues/issue_79.rb0000644000004100000410000000135613321132064017633 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' puts "Running amqp gem #{AMQP::VERSION}" AMQP.start do |conn| @amqp_connection = conn AMQP::Channel.new(@amqp_connection) do |channel| @amqp_channel = channel @amqp_exchange = @amqp_channel.topic AMQP::Queue.new(@amqp_channel, "") do |queue| queue.bind(@amqp_exchange, :routing_key => "a.*.pattern").subscribe(:ack => true) do |header, payload| p header.class p header.to_hash conn.close { EventMachine.stop } end end EventMachine.add_timer(0.3) do @amqp_exchange.publish "a message", :routing_key => "a.b.pattern" end end endamqp-1.8.0/examples/issues/issue_75.rb0000644000004100000410000000072313321132064017624 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' puts "Running amqp gem #{AMQP::VERSION}" AMQP.start(:host => "localhost") do |connection| channel = AMQP::Channel.new(connection) channel.fanout("logs.nad", :auto_delete => false) channel.fanout("logs.ad", :auto_delete => true) EM.add_timer(1) do connection.close do EM.stop { exit } end end endamqp-1.8.0/examples/issues/amq_protocol_issue_14.rb0000644000004100000410000000214313321132064022372 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' puts AMQP.start("amqp://guest:guest@dev.rabbitmq.com:5672") do |connection, open_ok| AMQP::Channel.new do |channel, open_ok| puts "Channel ##{channel.id} is now open!" channel.on_error do |ch, channel_close| puts "channel.close = #{channel_close.inspect}" puts "Handling a channel-level exception" end EventMachine.add_timer(0.4) do # this one works # binding = '12345678901234567890123456789012' # this one does not work binding = '123456789012345678901234567890123' queue = channel.queue 'test', :auto_delete => true, :durable => false queue.bind('amq.topic', :routing_key => binding) queue.unbind('amq.topic', :routing_key => binding) queue.unbind('amq.topic', :routing_key => binding) end end show_stopper = Proc.new do $stdout.puts "Stopping..." connection.close { EM.stop { exit } } end Signal.trap "INT", show_stopper EM.add_timer(2, show_stopper) end amqp-1.8.0/examples/issues/issue_80.rb0000644000004100000410000000173413321132064017623 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' puts "Running amqp gem #{AMQP::VERSION}" AMQP.start("amqp://guest:guest@localhost:5672") do |connection, open_ok| channel = AMQP::Channel.new(connection) channel.on_error { |ch, channel_close| puts "Error #{channel_close.inspect}" } queue = channel.queue("reset_test", :auto_delete => false, :durable=>true) exchange = channel.direct("foo") queue.bind(exchange) { puts 'Got bind ok'} EM.add_timer(2) do channel.reset queue = channel.queue("reset_test", :auto_delete => false, :durable=>true) exchange = channel.direct("foo") queue.unbind(exchange) { puts 'Got unbind ok'} end show_stopper = Proc.new do $stdout.puts "Stopping..." queue.delete exchange.delete connection.close { EM.stop { exit } } end Signal.trap "INT", show_stopper EM.add_timer(4, show_stopper) end amqp-1.8.0/examples/issues/amq_client_issue_7.rb0000644000004100000410000000127513321132064021736 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' puts "Running amqp gem #{AMQP::VERSION}" EM.run do AMQP.connect do |conn| @amqp_connection = conn AMQP::Channel.new(@amqp_connection) do |channel| @amqp_channel = channel @amqp_exchange = @amqp_channel.topic EventMachine.add_periodic_timer(0) do random_binary_string = Array.new(rand(1000)) { rand(256) }.pack('c*') random_binary_string.force_encoding('BINARY') p random_binary_string @amqp_exchange.publish(random_binary_string, :routing_key => "a.my.pattern") end end end end amqp-1.8.0/examples/error_handling/0000755000004100000410000000000013321132064017314 5ustar www-datawww-dataamqp-1.8.0/examples/error_handling/handling_vhost_misconfiguration_with_a_rescue_block.rb0000644000004100000410000000160113321132064032361 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' puts "=> Authentication failure/vhost misconfiguration handling with a rescue block" puts handler = Proc.new { |settings| puts "Failed to connect, as expected"; EM.stop } connection_settings = { :port => 5672, :vhost => "/a/b/c/d/#{rand}/#{Time.now.to_i}", :user => "amq_client_gem", :password => "amq_client_gem_password_that_is_incorrect #{Time.now.to_i}", :timeout => 0.3, :on_tcp_connection_failure => handler } begin AMQP.start(connection_settings) do |connection, open_ok| raise "This should not be reachable" end rescue AMQP::PossibleAuthenticationFailureError => afe puts "Authentication failed, as expected, caught #{afe.inspect}" EventMachine.stop if EventMachine.reactor_running? end amqp-1.8.0/examples/error_handling/channel_level_exception.rb0000644000004100000410000000306213321132064024517 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' puts "=> Queue redeclaration with different attributes results in a channel exception that is handled" puts AMQP.start("amqp://guest:guest@dev.rabbitmq.com:5672") do |connection, open_ok| AMQP::Channel.new do |channel, open_ok| puts "Channel ##{channel.id} is now open!" channel.on_error do |ch, channel_close| puts <<-ERR Handling a channel-level exception. AMQP class id : #{channel_close.class_id}, AMQP method id: #{channel_close.method_id}, Status code : #{channel_close.reply_code} Error message : #{channel_close.reply_text} ERR end EventMachine.add_timer(0.4) do # these two definitions result in a race condition. For sake of this example, # however, it does not matter. Whatever definition succeeds first, 2nd one will # cause a channel-level exception (because attributes are not identical) AMQP::Queue.new(channel, "amqpgem.examples.channel_exception", :auto_delete => true, :durable => false) do |queue| puts "#{queue.name} is ready to go" end AMQP::Queue.new(channel, "amqpgem.examples.channel_exception", :auto_delete => true, :durable => true) do |queue| puts "#{queue.name} is ready to go" end end end show_stopper = Proc.new do $stdout.puts "Stopping..." connection.close { EM.stop { exit } } end Signal.trap "INT", show_stopper EM.add_timer(2, show_stopper) end amqp-1.8.0/examples/error_handling/queue_name_violation.rb0000644000004100000410000000125213321132064024051 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' puts "=> Queue declaration uses name prefix amq.* reserved by the AMQP spec" puts AMQP.start("amqp://guest:guest@dev.rabbitmq.com:5672") do |connection, open_ok| AMQP::Channel.new do |channel, open_ok| puts "Channel ##{channel.id} is now open!" channel.on_error do |ch, close| puts "Handling a channel-level exception: #{close.reply_text}, code: #{close.reply_code}" end channel.queue("amq.queue") end EventMachine.add_timer(0.5) do connection.close { EventMachine.stop { exit } } end end amqp-1.8.0/examples/error_handling/basic_connection_failover.rb0000644000004100000410000000103713321132064025031 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' puts "=> Example of basic failover with AMQP::Session#reconnect_to" puts AMQP.start(:host => "localhost") do |connection, open_ok| connection.on_recovery do |conn, settings| puts "Connection recovered, now connected to dev.rabbitmq.com" end connection.on_tcp_connection_loss do |conn, settings| puts "Trying to reconnect..." conn.reconnect_to("amqp://dev.rabbitmq.com") end end amqp-1.8.0/examples/error_handling/channel_level_exception_with_multiple_channels_involved.rb0000644000004100000410000000260513321132064033250 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' puts "=> Queue redeclaration with different attributes results in a channel exception that is handled" puts AMQP.start("amqp://guest:guest@dev.rabbitmq.com:5672") do |connection, open_ok| ch1 = AMQP::Channel.new(connection) do |ch, open_ok| puts "Channel ##{ch.id} is now open!" end ch1.on_error do |ch, close| raise "Handling channel-level exception on channel with id of #{ch.id} (ch1)" end ch2 = AMQP::Channel.new(connection) do |ch, open_ok| puts "Channel ##{ch.id} is now open!" end ch2.on_error do |ch, close| puts "close: #{close.inspect}" puts "Handling channel-level exception on channel with id of #{ch.id} (ch2)" end EventMachine.add_timer(0.2) do AMQP::Queue.new(ch1, "amqpgem.examples.channel_exception", :auto_delete => true, :durable => false) do |queue| puts "#{queue.name} is ready to go" end end EventMachine.add_timer(0.6) do AMQP::Queue.new(ch2, "amqpgem.examples.channel_exception", :auto_delete => true, :durable => true) do |queue| puts "#{queue.name} is ready to go" end end show_stopper = Proc.new do $stdout.puts "Stopping..." connection.close { EM.stop { exit } } end Signal.trap "INT", show_stopper EM.add_timer(2, show_stopper) end amqp-1.8.0/examples/error_handling/tcp_connection_failure_with_a_callback.rb0000644000004100000410000000116613321132064027530 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' puts "=> TCP connection failure handling with a callback" puts handler = Proc.new { |settings| puts "Failed to connect, as expected"; EM.stop } connection_settings = { :port => 9689, :vhost => "/amq_client_testbed", :user => "amq_client_gem", :password => "amq_client_gem_password", :timeout => 0.3, :on_tcp_connection_failure => handler } AMQP.start(connection_settings) do |connection, open_ok| raise "This should not be reachable" end amqp-1.8.0/examples/error_handling/connection_loss_handler.rb0000644000004100000410000000205113321132064024533 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' puts "=> Connection loss is detected and handled" puts AMQP.start(:port => 5672, :vhost => "amq_client_testbed", :user => "amq_client_gem", :password => "amq_client_gem_password", :timeout => 0.3, :heartbeat => 1.0, :on_tcp_connection_failure => Proc.new { |settings| puts "Failed to connect, this was NOT expected"; EM.stop }) do |connection, open_ok| connection.on_tcp_connection_loss do |cl, settings| puts "tcp_connection_loss handler kicks in" cl.reconnect(false, 1) end show_stopper = Proc.new do $stdout.puts "Stopping..." connection.close { EventMachine.stop } end puts "Connected, authenticated. To really exercise this example, shut RabbitMQ down for a few seconds. If you don't it will exit gracefully in 30 seconds." Signal.trap "INT", show_stopper EM.add_timer(60, show_stopper) end amqp-1.8.0/examples/error_handling/connection_level_exception_with_objects.rb0000644000004100000410000000255013321132064030013 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' class ConnectionManager # # API # def connect(*args, &block) @connection = AMQP.connect(*args, &block) # combines Object#method and Method#to_proc to use object # method as a callback @connection.on_error(&method(:on_error)) end # connect(*args, &block) def on_error(connection, connection_close) puts "Handling a connection-level exception." puts puts "AMQP class id : #{connection_close.class_id}" puts "AMQP method id: #{connection_close.method_id}" puts "Status code : #{connection_close.reply_code}" puts "Error message : #{connection_close.reply_text}" end # on_error(connection, connection_close) end EventMachine.run do manager = ConnectionManager.new manager.connect(:host => '127.0.0.1', :port => 5672) do |connection| puts "Connected to RabbitMQ. Running #{AMQP::VERSION} version of the gem..." # send_frame is NOT part of the public API, but it is public for entities like AMQ::Client::Channel # and we use it here to trigger a connection-level exception. MK. connection.send_frame(AMQ::Protocol::Connection::TuneOk.encode(1000, 1024 * 128 * 1024, 10)) end # shut down after 2 seconds EventMachine.add_timer(2) { EventMachine.stop } end ././@LongLink0000644000000000000000000000016300000000000011603 Lustar rootrootamqp-1.8.0/examples/error_handling/automatically_recovering_hello_world_consumer_that_uses_a_server_named_queue.rbamqp-1.8.0/examples/error_handling/automatically_recovering_hello_world_consumer_that_uses_a_server_0000644000004100000410000000303613321132064034745 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' puts "=> Example of automatic AMQP channel and queues recovery" puts AMQP.start(:host => "localhost") do |connection, open_ok| connection.on_error do |ch, connection_close| puts "[connection closed] #{connection_close.reply_text}" end ch1 = AMQP::Channel.new(connection, 2, :auto_recovery => true) ch1.on_error do |ch, channel_close| raise channel_close.reply_text end connection.on_tcp_connection_loss do |conn, settings| puts "[network failure] Trying to reconnect..." conn.reconnect(false, 2) end queue = ch1.queue("", :auto_delete => true, :exclusive => true).bind("amq.fanout") queue.subscribe(:ack => true) do |metadata, payload| puts "[consumer1] => #{payload}" metadata.ack end consumer2 = AMQP::Consumer.new(ch1, queue) consumer2.consume.on_delivery do |metadata, payload| puts "[conusmer2] => #{payload}" metadata.ack end show_stopper = Proc.new { connection.disconnect { puts "Disconnected. Exiting…"; EventMachine.stop } } Signal.trap "TERM", show_stopper Signal.trap "INT", show_stopper EM.add_timer(45, show_stopper) puts "This example needs another script/app to publish messages to amq.fanout. See examples/error_handling/hello_world_producer.rb for example" puts "Connected, authenticated. To really exercise this example, shut RabbitMQ down for a few seconds. If you don't it will exit gracefully in 45 seconds." end amqp-1.8.0/examples/error_handling/hello_world_producer.rb0000644000004100000410000000342113321132064024056 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' puts "=> Auxiliary script that tests automatically recovering message consumer(s)" puts AMQP.start(:host => ENV.fetch("BROKER_HOST", "localhost")) do |connection, open_ok| puts "Connected to #{connection.hostname}" connection.on_error do |ch, connection_close| raise connection_close.reply_text end connection.on_tcp_connection_loss do |conn, settings| puts "[network failure] Trying to reconnect..." conn.reconnect(false, 2) end ch1 = AMQP::Channel.new(connection, :auto_recovery => true) ch1.on_error do |ch, channel_close| raise channel_close.reply_text end exchange = ch1.fanout("amq.fanout", :durable => true) EventMachine.add_periodic_timer(0.9) do puts "Publishing via default exchange..." # messages must be routable & there must be at least one consumer. ch1.default_exchange.publish("Routed via default_exchange", :routing_key => "amqpgem.examples.autorecovery.queue") end EventMachine.add_periodic_timer(0.8) do puts "Publishing via amq.fanout..." # messages must be routable & there must be at least one consumer. exchange.publish("Routed via amq.fanout", :mandatory => true) end show_stopper = Proc.new { connection.disconnect { puts "Disconnected. Exiting…"; EventMachine.stop } } Signal.trap "TERM", show_stopper Signal.trap "INT", show_stopper EM.add_timer(ENV.fetch("TIMER", 15), show_stopper) puts "This example a helper that publishes messages to amq.fanout. Use together with examples/error_handling/automatically_recovering_hello_world_consumer.rb." puts "This example terminates in 15 seconds and needs MANUAL RESTART when connection fails" end amqp-1.8.0/examples/error_handling/reopening_a_channel_after_channel_level_exception.rb0000644000004100000410000000404713321132064031742 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' puts "=> Queue redeclaration with different attributes results in a channel exception that is handled" puts EventMachine.run do connection = AMQP.connect(:port => ENV.fetch("PORT", 5672)) AMQP::Channel.new(connection) do |channel, open_ok| puts "Channel ##{channel.id} is now open!" connection.on_error do |conn, connection_close| puts <<-ERR Handling a connection-level exception: connection_close.reply_text: #{connection_close.reply_text} ERR end channel.on_error do |ch, channel_close| puts "Handling a channel-level exception: #{channel_close.reply_code} #{channel_close.reply_text}" ch.reuse end # initial operations that cause the channel-level exception handled above EventMachine.add_timer(0.5) do # these two definitions result in a race condition. For sake of this example, # however, it does not matter. Whatever definition succeeds first, 2nd one will # cause a channel-level exception (because attributes are not identical) AMQP::Queue.new(channel, "amqpgem.examples.channel_exception", :exclusive => true, :durable => false) AMQP::Queue.new(channel, "amqpgem.examples.channel_exception", :auto_delete => true, :durable => true) end EventMachine.add_timer(2.0) do puts "Resuming with reopened channel #{channel.id}" q = channel.queue("amqpgem.examples.channel_exception.q1", :exclusive => true).bind("amq.fanout").subscribe do |meta, payload| puts "Consumed #{payload}" end end EventMachine.add_periodic_timer(3.0) do puts "Publishing a message..." x = channel.fanout("amq.fanout") x.publish("xyz", :routing_key => "amqpgem.examples.channel_exception.q1") end end show_stopper = Proc.new do $stdout.puts "Stopping..." connection.close { EM.stop { exit } } end Signal.trap "INT", show_stopper # EM.add_timer(2, show_stopper) end amqp-1.8.0/examples/error_handling/automatic_recovery_of_channel_and_queues.rb0000644000004100000410000000256613321132064030143 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' puts "=> Example of automatic AMQP channel and queues recovery" puts AMQP.start(:host => "localhost") do |connection, open_ok| connection.on_error do |ch, connection_close| raise connection_close.reply_text end ch1 = AMQP::Channel.new(connection, 2, :auto_recovery => true) ch1.on_error do |ch, channel_close| raise channel_close.reply_text end if ch1.auto_recovering? puts "Channel #{ch1.id} IS auto-recovering" end connection.on_tcp_connection_loss do |conn, settings| puts "[network failure] Trying to reconnect..." conn.reconnect(false, 2) end ch1.queue("amqpgem.examples.queue1", :auto_delete => true).bind("amq.fanout") ch1.queue("amqpgem.examples.queue2", :auto_delete => true).bind("amq.fanout") ch1.queue("amqpgem.examples.queue3", :auto_delete => true).bind("amq.fanout").subscribe do |metadata, payload| end show_stopper = Proc.new { connection.disconnect { puts "Disconnected. Exiting…"; EventMachine.stop } } Signal.trap "TERM", show_stopper Signal.trap "INT", show_stopper EM.add_timer(30, show_stopper) puts "Connected, authenticated. To really exercise this example, shut RabbitMQ down for a few seconds. If you don't it will exit gracefully in 30 seconds." end amqp-1.8.0/examples/error_handling/connection_level_exception.rb0000644000004100000410000000170713321132064025252 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' EventMachine.run do AMQP.connect(:host => '127.0.0.1', :port => 5672) do |connection| puts "Connected to RabbitMQ. Running #{AMQP::VERSION} version of the gem..." connection.on_error do |conn, connection_close| puts <<-ERR Handling a connection-level exception. AMQP class id : #{connection_close.class_id}, AMQP method id: #{connection_close.method_id}, Status code : #{connection_close.reply_code} Error message : #{connection_close.reply_text} ERR EventMachine.stop end # send_frame is NOT part of the public API, but it is public for entities like AMQ::Client::Channel # and we use it here to trigger a connection-level exception. MK. connection.send_frame(AMQ::Protocol::Connection::TuneOk.encode(1000, 1024 * 128 * 1024, 10)) end end amqp-1.8.0/examples/error_handling/manual_connection_and_channel_recovery.rb0000644000004100000410000000325013321132064027565 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' class ConnectionInterruptionHandler # # API # def handle(connection) puts "[network failure] Connection #{connection} detected connection interruption" end end puts "=> Example of AMQP connection & channel recovery API in action" puts AMQP.start(:host => "localhost") do |connection, open_ok| unless connection.auto_recovering? puts "Connection IS NOT auto-recovering..." end ch1 = AMQP::Channel.new(connection) ch1.on_error do |ch, channel_close| raise channel_close.reply_text end unless ch1.auto_recovering? puts "Channel #{ch1.id} IS NOT auto-recovering" end ch1.on_connection_interruption do |c| puts "[network failure] Channel #{c.id} reacted to connection interruption" end ch1.on_recovery do |c| puts "[recovery] Channel #{c.id} has recovered" end connection.on_tcp_connection_loss do |conn, settings| puts "[network failure] Trying to reconnect..." conn.reconnect(false, 2) end handler = ConnectionInterruptionHandler.new connection.on_connection_interruption(&handler.method(:handle)) connection.on_recovery do |conn, settings| puts "[recovery] Connection has recovered" end show_stopper = Proc.new { connection.disconnect { puts "Disconnected. Exiting…"; EventMachine.stop } } Signal.trap "TERM", show_stopper Signal.trap "INT", show_stopper EM.add_timer(30, show_stopper) puts "Connected, authenticated. To really exercise this example, shut RabbitMQ down for a few seconds. If you don't it will exit gracefully in 30 seconds." end amqp-1.8.0/examples/error_handling/automatically_recovering_hello_world_consumer.rb0000644000004100000410000000315313321132064031243 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' puts "=> Example of automatic AMQP channel and queues recovery" puts AMQP.start(:host => ENV.fetch("BROKER_HOST", "localhost")) do |connection, open_ok| connection.on_error do |ch, connection_close| puts "[connection closed] #{connection_close.reply_text}" end ch1 = AMQP::Channel.new(connection, :auto_recovery => true) ch1.on_error do |ch, channel_close| raise channel_close.reply_text end connection.on_tcp_connection_loss do |conn, settings| puts "[network failure] Trying to reconnect..." conn.reconnect(false, 2) end queue = ch1.queue("amqpgem.examples.autorecovery.queue", :auto_delete => false, :durable => true).bind("amq.fanout") queue.subscribe(:ack => true) do |metadata, payload| puts "[consumer1] => #{payload}" metadata.ack end consumer2 = AMQP::Consumer.new(ch1, queue) consumer2.consume.on_delivery do |metadata, payload| puts "[conusmer2] => #{payload}" metadata.ack end show_stopper = Proc.new { connection.disconnect { puts "Disconnected. Exiting…"; EventMachine.stop } } Signal.trap "TERM", show_stopper Signal.trap "INT", show_stopper EM.add_timer(ENV.fetch("TIMER", 45), show_stopper) puts "This example needs another script/app to publish messages to amq.fanout. See examples/error_handling/hello_world_producer.rb for example" puts "Connected, authenticated. To really exercise this example, shut RabbitMQ down for a few seconds. If you don't it will exit gracefully in 45 seconds." end amqp-1.8.0/examples/error_handling/global_channel_level_exception_handler.rb0000644000004100000410000000331413321132064027534 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' puts "=> Queue redeclaration with different attributes results in a channel exception that is handled by a global callback we carry from 0.7.x days" puts puts <<-MSG WARNING!! ACHTUNG!! AVIS!! AVISO!! Poorly designed API use ahead!! Please never ever use global error handler demonstrated below! It is a brilliant decision from early days of the library and we have to carry it along for backwards compatibility. Global state is programming is usually a pain. Global error handling is a true Hell. You have been warned. MSG puts puts AMQP.start("amqp://guest:guest@dev.rabbitmq.com:5672") do |connection, open_ok| AMQP::Channel.new do |channel, open_ok| puts "Channel ##{channel.id} is now open!" channel.on_error do |ch, channel_close| puts "Handling channel-level exception at instance level" end AMQP::Channel.on_error do |ch, channel_close| puts "Handling channel-level exception at GLOBAL level. Jeez. Message is #{channel_close.reply_text}" end EventMachine.add_timer(0.4) do q1 = AMQP::Queue.new(channel, "amqpgem.examples.channel_exception", :auto_delete => true, :durable => false) do |queue| puts "#{queue.name} is ready to go" end q2 = AMQP::Queue.new(channel, "amqpgem.examples.channel_exception", :auto_delete => true, :durable => true) do |queue| puts "#{queue.name} is ready to go" end end end show_stopper = Proc.new do $stdout.puts "Stopping..." connection.close { EM.stop { exit } } end Signal.trap "INT", show_stopper EM.add_timer(2, show_stopper) end amqp-1.8.0/examples/error_handling/handling_authentication_failure_with_a_rescue_block.rb0000644000004100000410000000153413321132064032311 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' puts "=> Authentication failure handling with a rescue block" puts handler = Proc.new { |settings| puts "Failed to connect, as expected"; EM.stop } connection_settings = { :port => 5672, :vhost => "/amq_client_testbed", :user => "amq_client_gem", :password => "amq_client_gem_password_that_is_incorrect #{Time.now.to_i}", :timeout => 0.3, :on_tcp_connection_failure => handler } begin AMQP.start(connection_settings) do |connection, open_ok| raise "This should not be reachable" end rescue AMQP::PossibleAuthenticationFailureError => afe puts "Authentication failed, as expected, caught #{afe.inspect}" EventMachine.stop if EventMachine.reactor_running? end amqp-1.8.0/examples/error_handling/tcp_connection_failure_handling_with_a_rescue_block.rb0000644000004100000410000000117713321132064032302 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' puts "=> TCP connection failure handling with a rescue statement" puts connection_settings = { :port => 9689, :vhost => "/amq_client_testbed", :user => "amq_client_gem", :password => "amq_client_gem_password", :timeout => 0.3 } begin AMQP.start(connection_settings) do |connection, open_ok| raise "This should not be reachable" end rescue AMQP::TCPConnectionFailed => e puts "Caught AMQP::TCPConnectionFailed => TCP connection failed, as expected." end amqp-1.8.0/examples/error_handling/handling_authentication_failure_with_a_callback.rb0000644000004100000410000000166313321132064031410 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' puts "=> Authentication failure handling with a callback" puts handler = Proc.new { |settings| puts "Failed to connect, as expected"; EM.stop } connection_settings = { :port => 5672, :vhost => "/amq_client_testbed", :user => "amq_client_gem", :password => "amq_client_gem_password_that_is_incorrect #{Time.now.to_i}", :timeout => 0.3, :on_tcp_connection_failure => handler, :on_possible_authentication_failure => Proc.new { |settings| puts "Authentication failed, as expected, settings are: #{settings.inspect}" EM.stop } } AMQP.start(connection_settings) do |connection, open_ok| raise "This should not be reachable" end amqp-1.8.0/examples/error_handling/insufficient_permissions.rb0000644000004100000410000000314413321132064024764 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' puts "=> Queue redeclaration with different attributes results in a channel exception that is handled" puts AMQP.start(:vhost => "amqp_gem_testbed", :username => "amqp_gem_reader", :password => "reader_password") do |connection, open_ok| AMQP::Channel.new do |channel, open_ok| puts "Channel ##{channel.id} is now open!" channel.on_error do |ch, channel_close| puts <<-ERR Handling a channel-level exception. AMQP class id : #{channel_close.class_id}, AMQP method id: #{channel_close.method_id}, Status code : #{channel_close.reply_code} Error message : #{channel_close.reply_text} ERR end EventMachine.add_timer(0.4) do # these two definitions result in a race condition. For sake of this example, # however, it does not matter. Whatever definition succeeds first, 2nd one will # cause a channel-level exception (because attributes are not identical) AMQP::Queue.new(channel, "amqpgem.examples.channel_exception", :auto_delete => true, :durable => false) do |queue| puts "#{queue.name} is ready to go" end AMQP::Queue.new(channel, "amqpgem.examples.channel_exception", :auto_delete => true, :durable => true) do |queue| puts "#{queue.name} is ready to go" end end end show_stopper = Proc.new do $stdout.puts "Stopping..." connection.close { EM.stop { exit } } end Signal.trap "INT", show_stopper EM.add_timer(2, show_stopper) end amqp-1.8.0/examples/error_handling/queue_exclusivity_violation.rb0000644000004100000410000000212113321132064025515 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' puts "=> Queue exclusivity violation results " puts EventMachine.run do connection1 = AMQP.connect("amqp://guest:guest@dev.rabbitmq.com") channel1 = AMQP::Channel.new(connection1) connection2 = AMQP.connect("amqp://guest:guest@dev.rabbitmq.com") channel2 = AMQP::Channel.new(connection2) channel1.on_error do |ch, close| puts "Handling a channel-level exception on channel1: #{close.reply_text}, #{close.reply_code}" end channel2.on_error do |ch, close| puts "Handling a channel-level exception on channel2: #{close.reply_text}, #{close.reply_code}" end name = "amqpgem.examples.queue" channel1.queue(name, :auto_delete => true, :exclusive => true) # declare a queue with the same name on a different connection channel2.queue(name, :auto_delete => true, :exclusive => true) EventMachine.add_timer(3.5) do connection1.close { connection2.close { EventMachine.stop { exit } } } end end amqp-1.8.0/examples/hello_world_with_eventmachine_in_a_separate_thread.rb0000644000004100000410000000161113321132064027137 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../lib", __FILE__)) require 'amqp' t = Thread.new { EventMachine.run } if defined?(JRUBY_VERSION) # on the JVM, event loop startup takes longer and .next_tick behavior # seem to be a bit different. Blocking current thread for a moment helps. sleep 0.5 end EventMachine.next_tick { connection = AMQP.connect(:host => '127.0.0.1') puts "Connected to RabbitMQ. Running #{AMQP::VERSION} version of the gem..." channel = AMQP::Channel.new(connection) queue = channel.queue("amqpgem.examples.hello_world", :auto_delete => true) exchange = channel.direct("") queue.subscribe do |payload| puts "Received a message: #{payload}. Disconnecting..." connection.close { EM.stop { exit } } end exchange.publish "Hello, world!", :routing_key => queue.name } t.join amqp-1.8.0/examples/publishing/0000755000004100000410000000000013321132064016463 5ustar www-datawww-dataamqp-1.8.0/examples/publishing/publishing_callback.rb0000644000004100000410000000331013321132064022765 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' puts "=> Using a callback to #publish. It is run on the _next_ EventMachine loop run." puts EventMachine.run do connection = AMQP.connect(:host => '127.0.0.1') channel = AMQP::Channel.new(connection) channel.on_error do |ch, channel_close| puts "Channel-level error: #{channel_close.reply_text}, shutting down..." connection.close { EventMachine.stop } end queue = channel.queue("amqpgem.examples.publishing.queue1", :auto_delete => true) exchange = channel.fanout("amqpgem.examples.topic", :durable => true, :auto_delete => true) queue.bind(exchange, :routing_key => "some_topic") # Don't be deceived: this callback is run on the next event loop tick. There is no guarantee that your # data was sent: there is buffering going on on multiple layers (C++ core of EventMachine, libc functions, # kernel uses buffering for many I/O system calls). # # This callback is simply for convenience. In a distributed environment, the only way to know when your data # is sent is when you receive an acknowledgement. TCP works that way. MK. 100.times do |i| exchange.publish("hello world #{i}", :routing_key => "some_topic", :persistent => true) do puts "Callback #{i} has fired" end end exchange.publish("hello world 101", :routing_key => "some_topic", :persistent => false) do puts "Callback 101 has fired" end exchange.publish("hello world 102", :routing_key => "some_topic", :persistent => true) do puts "Callback 102 has fired" end EventMachine.add_timer(1) do connection.close { EventMachine.stop } end end amqp-1.8.0/examples/publishing/simplistic_scatter_gather.rb0000644000004100000410000000350313321132064024250 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' AMQP.start do |session| ch = AMQP::Channel.new(session) exchange = ch.fanout("amq.fanout") reply_exchange = ch.default_exchange ch.queue("", :exclusive => true) do |q1| q1.bind(exchange).subscribe do |header, payload| v = rand puts "Replying to #{header.message_id} with #{v}" reply_exchange.publish(v, :routing_key => header.reply_to, :message_id => header.message_id) end end ch.queue("", :exclusive => true) do |q2| q2.bind(exchange).subscribe do |header, payload| v = rand puts "Replying to #{header.message_id} with #{v}" reply_exchange.publish(v, :routing_key => header.reply_to, :message_id => header.message_id) end end ch.queue("", :exclusive => true) do |q3| q3.bind(exchange).subscribe do |header, payload| v = rand puts "Replying to #{header.message_id} with #{v}" reply_exchange.publish(v, :routing_key => header.reply_to, :message_id => header.message_id) end end requests = Hash.new EventMachine.add_timer(0.5) do ch.queue("", :exlusive => true) do |q| q.subscribe do |header, payload| requests[header.message_id].push(payload) puts "Got a reply for #{header.message_id}" if requests[header.message_id].size == 3 puts "Gathered all 3 responses: #{requests[header.message_id].join(', ')}" requests[header.message_id].clear end end message_id = "__message #{rand}__" requests[message_id] = Array.new exchange.publish("a request", :reply_to => q.name, :message_id => message_id) end end EventMachine.add_timer(2) do session.close { EventMachine.stop } end end amqp-1.8.0/examples/publishing/returned_messages.rb0000644000004100000410000000142213321132064022526 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' puts "=> Handling returned messages" puts AMQP.start(:host => '127.0.0.1') do |connection| channel = AMQP.channel channel.on_error { |ch, channel_close| EventMachine.stop; raise "channel error: #{channel_close.reply_text}" } exchange = channel.fanout("amq.fanout") exchange.on_return do |basic_return, metadata, payload| puts "#{payload} was returned! reply_code = #{basic_return.reply_code}, reply_text = #{basic_return.reply_text}" end EventMachine.add_timer(0.3) { 10.times do |i| exchange.publish("Message ##{i}") end } EventMachine.add_timer(2) { connection.close { EventMachine.stop } } end amqp-1.8.0/examples/publishing/publishing_a_one_off_message.rb0000644000004100000410000000247513321132064024663 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' puts "=> Publishing and immediately stopping the event loop in the callback" puts # WARNING: this example is born out of http://bit.ly/j6v1Uz (#67) and # by no means a demonstration of how you should go about publishing one-off messages. # If durability is a concern, please read our "Durability and message persistence" guide at # http://bit.ly/lQP1Al EventMachine.run do connection = AMQP.connect(:host => '127.0.0.1') channel = AMQP::Channel.new(connection) channel.on_error do |ch, channel_close| puts "Channel-level error: #{channel_close.reply_text}, shutting down..." connection.close { EventMachine.stop } end # topic exchange is used just as example. Often it is more convenient to use default exchange, # see http://bit.ly/amqp-gem-default-exchange exchange = channel.topic("a.topic", :durable => true, :auto_delete => true) queue = channel.queue("a.queue", :auto_delete => true).bind(exchange, :routing_key => "events.#") exchange.publish('hello world', :routing_key => "events.hits.homepage", :persistent => true, :nowait => false) do puts "About to disconnect..." connection.close { EventMachine.stop } end end amqp-1.8.0/examples/inspecting_server_information.rb0000644000004100000410000000241613321132064023005 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../lib", __FILE__)) require 'amqp' EventMachine.run do AMQP.connect(:host => '127.0.0.1') do |connection| puts puts "Connected to #{connection.hostname}:#{connection.port}/#{connection.vhost}" puts puts "Client properties:" puts puts connection.client_properties.inspect puts "Server properties:" puts puts connection.server_properties.inspect puts "Server capabilities:" puts puts connection.server_capabilities.inspect puts "Broker product: #{connection.broker.product}, version: #{connection.broker.version}" puts "Connected to RabbitMQ? #{connection.broker.rabbitmq?}" puts puts "Broker supports publisher confirms? #{connection.broker.supports_publisher_confirmations?}" puts puts "Broker supports basic.nack? #{connection.broker.supports_basic_nack?}" puts puts "Broker supports consumer cancel notifications? #{connection.broker.supports_consumer_cancel_notifications?}" puts puts "Broker supports exchange-to-exchange bindings? #{connection.broker.supports_exchange_to_exchange_bindings?}" connection.disconnect { EventMachine.stop } end # AMQP.connect end # EventMachine.run amqp-1.8.0/examples/guides/0000755000004100000410000000000013321132064015577 5ustar www-datawww-dataamqp-1.8.0/examples/guides/getting_started/0000755000004100000410000000000013321132064020766 5ustar www-datawww-dataamqp-1.8.0/examples/guides/getting_started/02_hello_world_dslified.rb0000644000004100000410000000113313321132064025767 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "rubygems" require "amqp" EventMachine.run do AMQP.connect(:host => '127.0.0.1') do |connection| puts "Connected to RabbitMQ. Running #{AMQP::VERSION} version of the gem..." channel = AMQP::Channel.new(connection) channel.queue("amqpgem.examples.helloworld", :auto_delete => true).subscribe do |payload| puts "Received a message: #{payload}. Disconnecting..." connection.close { EM.stop { exit } } end channel.direct("").publish "Hello, world!", :routing_key => "amqpgem.examples.helloworld" end end amqp-1.8.0/examples/guides/getting_started/03_blabbr.rb0000644000004100000410000000143513321132064023044 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "rubygems" require "amqp" AMQP.start("amqp://dev.rabbitmq.com:5672") do |connection| channel = AMQP::Channel.new(connection) exchange = channel.fanout("nba.scores") channel.queue("joe", :auto_delete => true).bind(exchange).subscribe do |payload| puts "#{payload} => joe" end channel.queue("aaron", :auto_delete => true).bind(exchange).subscribe do |payload| puts "#{payload} => aaron" end channel.queue("bob", :auto_delete => true).bind(exchange).subscribe do |payload| puts "#{payload} => bob" end exchange.publish("BOS 101, NYK 89").publish("ORL 85, ALT 88") # disconnect & exit after 2 seconds EventMachine.add_timer(2) do exchange.delete connection.close { EM.stop { exit } } end end amqp-1.8.0/examples/guides/getting_started/01_hello_world.rb0000644000004100000410000000112613321132064024125 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "rubygems" require "amqp" EventMachine.run do connection = AMQP.connect(:host => '127.0.0.1') puts "Connected to RabbitMQ. Running #{AMQP::VERSION} version of the gem..." channel = AMQP::Channel.new(connection) queue = channel.queue("amqpgem.examples.helloworld", :auto_delete => true) exchange = channel.direct("") queue.subscribe do |payload| puts "Received a message: #{payload}. Disconnecting..." connection.close { EM.stop { exit } } end exchange.publish "Hello, world!", :routing_key => queue.name end amqp-1.8.0/examples/guides/getting_started/04_weathr.rb0000644000004100000410000000514113321132064023111 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "rubygems" require "amqp" EventMachine.run do AMQP.connect do |connection| channel = AMQP::Channel.new(connection) exchange = channel.topic("pub/sub", :auto_delete => true) # Subscribers. channel.queue("", :exclusive => true) do |queue| queue.bind(exchange, :routing_key => "americas.north.#").subscribe do |headers, payload| puts "An update for North America: #{payload}, routing key is #{headers.routing_key}" end end channel.queue("americas.south").bind(exchange, :routing_key => "americas.south.#").subscribe do |headers, payload| puts "An update for South America: #{payload}, routing key is #{headers.routing_key}" end channel.queue("us.california").bind(exchange, :routing_key => "americas.north.us.ca.*").subscribe do |headers, payload| puts "An update for US/California: #{payload}, routing key is #{headers.routing_key}" end channel.queue("us.tx.austin").bind(exchange, :routing_key => "#.tx.austin").subscribe do |headers, payload| puts "An update for Austin, TX: #{payload}, routing key is #{headers.routing_key}" end channel.queue("it.rome").bind(exchange, :routing_key => "europe.italy.rome").subscribe do |headers, payload| puts "An update for Rome, Italy: #{payload}, routing key is #{headers.routing_key}" end channel.queue("asia.hk").bind(exchange, :routing_key => "asia.southeast.hk.#").subscribe do |headers, payload| puts "An update for Hong Kong: #{payload}, routing key is #{headers.routing_key}" end EM.add_timer(1) do exchange.publish("San Diego update", :routing_key => "americas.north.us.ca.sandiego"). publish("Berkeley update", :routing_key => "americas.north.us.ca.berkeley"). publish("San Francisco update", :routing_key => "americas.north.us.ca.sanfrancisco"). publish("New York update", :routing_key => "americas.north.us.ny.newyork"). publish("São Paolo update", :routing_key => "americas.south.brazil.saopaolo"). publish("Hong Kong update", :routing_key => "asia.southeast.hk.hongkong"). publish("Kyoto update", :routing_key => "asia.southeast.japan.kyoto"). publish("Shanghai update", :routing_key => "asia.southeast.prc.shanghai"). publish("Rome update", :routing_key => "europe.italy.roma"). publish("Paris update", :routing_key => "europe.france.paris") end show_stopper = Proc.new { connection.close do EM.stop end } EM.add_timer(2, show_stopper) end end amqp-1.8.0/examples/guides/queues/0000755000004100000410000000000013321132064017106 5ustar www-datawww-dataamqp-1.8.0/examples/guides/queues/02b_declaring_a_durable_shared_queue.rb0000644000004100000410000000057513321132064026565 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "rubygems" require "amqp" # Declaring a durable shared queue using AMQP::Channel#queue method AMQP.start("amqp://guest:guest@dev.rabbitmq.com") do |connection, open_ok| channel = AMQP::Channel.new(connection) queue = channel.queue("images.resize", :durable => true) connection.close { EventMachine.stop { exit } } end amqp-1.8.0/examples/guides/queues/09_unbinding_from_exchange.rb0000644000004100000410000000126013321132064024604 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "rubygems" require "amqp" AMQP.start("amqp://guest:guest@dev.rabbitmq.com") do |connection, open_ok| channel = AMQP::Channel.new(connection) channel.on_error do |ch, channel_close| raise "Channel-level exception: #{channel_close.reply_text}" end exchange = channel.fanout("amq.fanout") channel.queue("", :auto_delete => true, :exclusive => true) do |queue, declare_ok| queue.bind(exchange) EventMachine.add_timer(0.5) do queue.unbind(exchange) do |_| puts "Unbound. Shutting down..." connection.close { EventMachine.stop } end end # EventMachine.add_timer end # channel.queue end amqp-1.8.0/examples/guides/queues/04_bind_a_queue_using_exchange_instance.rb0000644000004100000410000000102213321132064027304 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "rubygems" require "amqp" # Binding a queue to an exchange AMQP.start("amqp://guest:guest@dev.rabbitmq.com") do |connection, open_ok| channel = AMQP::Channel.new(connection) exchange = channel.fanout("amq.fanout") channel.queue("", :auto_delete => true, :exclusive => true) do |queue, declare_ok| queue.bind(exchange) do |bind_ok| puts "Just bound #{queue.name} to #{exchange.name}" end connection.close { EventMachine.stop { exit } } end end amqp-1.8.0/examples/guides/queues/11_deleting_a_queue.rb0000644000004100000410000000120313321132064023227 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "rubygems" require "amqp" AMQP.start("amqp://guest:guest@dev.rabbitmq.com") do |connection, open_ok| channel = AMQP::Channel.new(connection) channel.on_error do |ch, channel_close| raise "Channel-level exception: #{channel_close.reply_text}" end exchange = channel.fanout("amq.fanout") channel.queue("", :auto_delete => true, :exclusive => true) do |queue, declare_ok| EventMachine.add_timer(0.5) do queue.delete do puts "Deleted #{queue.name}" connection.close { EventMachine. stop } end end # EventMachine.add_timer end # channel.queue end amqp-1.8.0/examples/guides/queues/03a_declaring_a_temporary_exclusive_queue.rb0000644000004100000410000000067213321132064027730 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "rubygems" require "amqp" # Declaring a temporary exclusive queue AMQP.start("amqp://guest:guest@dev.rabbitmq.com") do |connection, open_ok| channel = AMQP::Channel.new(connection) AMQP::Queue.new(channel, "", :auto_delete => true, :exclusive => true) do |queue, declare_ok| puts "#{queue.name} is ready to go." connection.close { EventMachine.stop { exit } } end end amqp-1.8.0/examples/guides/queues/06_subscribe_to_receive_messages.rb0000644000004100000410000000113613321132064026015 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "rubygems" require "amqp" AMQP.start("amqp://guest:guest@dev.rabbitmq.com") do |connection, open_ok| channel = AMQP::Channel.new(connection) exchange = channel.fanout("amq.fanout") channel.queue("", :auto_delete => true, :exclusive => true) do |queue| queue.bind(exchange).subscribe do |metadata, payload| puts "Received a message: #{payload.inspect}. Shutting down..." connection.close { EventMachine.stop } end EventMachine.add_timer(0.2) do puts "=> Publishing..." exchange.publish("Ohai!") end end end amqp-1.8.0/examples/guides/queues/07_fetch_a_message_from_the_queue.rb0000644000004100000410000000137613321132064026134 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "rubygems" require "amqp" AMQP.start("amqp://guest:guest@dev.rabbitmq.com") do |connection, open_ok| channel = AMQP::Channel.new(connection) exchange = channel.fanout("amq.fanout") channel.queue("", :auto_delete => true, :exclusive => true) do |queue, declare_ok| queue.bind(exchange) puts "Bound. Publishing a message..." exchange.publish("Ohai!") EventMachine.add_timer(0.5) do queue.pop do |metadata, payload| if payload puts "Fetched a message: #{payload.inspect}, content_type: #{metadata.content_type}. Shutting down..." else puts "No messages in the queue" end connection.close { EventMachine.stop } end end end end amqp-1.8.0/examples/guides/queues/10_purge_a_queue.rb0000644000004100000410000000120213321132064022554 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "rubygems" require "amqp" AMQP.start("amqp://guest:guest@dev.rabbitmq.com:5672") do |connection, open_ok| channel = AMQP::Channel.new(connection) channel.on_error do |ch, channel_close| raise "Channel-level exception: #{channel_close.reply_text}" end exchange = channel.fanout("amq.fanout") channel.queue("", :auto_delete => true, :exclusive => true) do |queue, declare_ok| queue.purge do |_| puts "Purged #{queue.name}" end EventMachine.add_timer(0.5) do connection.close { EventMachine.stop } end # EventMachine.add_timer end # channel.queue end amqp-1.8.0/examples/guides/queues/08_unsubscribing_a_consumer.rb0000644000004100000410000000116513321132064025035 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "rubygems" require "amqp" AMQP.start("amqp://guest:guest@dev.rabbitmq.com") do |connection, open_ok| channel = AMQP::Channel.new(connection) exchange = channel.fanout("amq.fanout") channel.queue("", :auto_delete => true, :exclusive => true) do |queue, declare_ok| queue.bind(exchange).subscribe do |headers, payload| puts "Received a new message" end EventMachine.add_timer(0.3) do queue.unsubscribe puts "Unsubscribed. Shutting down..." connection.close { EventMachine.stop } end # EventMachine.add_timer end # channel.queue end amqp-1.8.0/examples/guides/queues/01b_declaring_a_queue_using_queue_constructor.rb0000644000004100000410000000067513321132064030617 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "rubygems" require "amqp" # Declaring a queue with explicitly given name using AMQP::Queue constructor AMQP.start("amqp://guest:guest@dev.rabbitmq.com") do |connection, open_ok| channel = AMQP::Channel.new(connection) queue = AMQP::Queue.new(channel, "images.resize", :auto_delete => true) puts "#{queue.name} is ready to go." connection.close { EventMachine.stop { exit } } end amqp-1.8.0/examples/guides/queues/01a_declaring_a_server_named_queue_using_queue_constructor.rb0000644000004100000410000000067613321132064033351 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "rubygems" require "amqp" # Declaring a queue with server-generated name using AMQP::Queue constructor AMQP.start("amqp://guest:guest@dev.rabbitmq.com") do |connection, open_ok| channel = AMQP::Channel.new(connection) AMQP::Queue.new(channel, "", :auto_delete => true) do |queue| puts "#{queue.name} is ready to go." connection.close { EventMachine.stop { exit } } end end amqp-1.8.0/examples/guides/queues/13_objects_that_consume_messages_take_two.rb0000644000004100000410000000367713321132064027741 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "rubygems" require "amqp" class Consumer # # API # def handle_message(metadata, payload) puts "Received a message: #{payload}, content_type = #{metadata.content_type}" end # handle_message(metadata, payload) end class Worker # # API # def initialize(channel, queue_name = AMQ::Protocol::EMPTY_STRING, consumer = Consumer.new) @queue_name = queue_name @channel = channel @channel.on_error(&method(:handle_channel_exception)) @consumer = consumer end # initialize def start @queue = @channel.queue(@queue_name, :exclusive => true) @queue.subscribe(&@consumer.method(:handle_message)) end # start # # Implementation # def handle_channel_exception(channel, channel_close) puts "Oops... a channel-level exception: code = #{channel_close.reply_code}, message = #{channel_close.reply_text}" end # handle_channel_exception(channel, channel_close) end class Producer # # API # def initialize(channel, exchange) @channel = channel @exchange = exchange end # initialize(channel, exchange) def publish(message, options = {}) @exchange.publish(message, options) end # publish(message, options = {}) # # Implementation # def handle_channel_exception(channel, channel_close) puts "Oops... a channel-level exception: code = #{channel_close.reply_code}, message = #{channel_close.reply_text}" end # handle_channel_exception(channel, channel_close) end AMQP.start("amqp://guest:guest@dev.rabbitmq.com") do |connection, open_ok| channel = AMQP::Channel.new(connection) worker = Worker.new(channel, "amqpgem.objects.integration") worker.start producer = Producer.new(channel, channel.default_exchange) puts "Publishing..." producer.publish("Hello, world", :routing_key => "amqpgem.objects.integration") # stop in 2 seconds EventMachine.add_timer(2.0) { connection.close { EventMachine.stop } } end amqp-1.8.0/examples/guides/queues/03b_declaring_a_temporary_exclusive_queue.rb0000644000004100000410000000070013321132064027721 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "rubygems" require "amqp" # Declaring a temporary exclusive queue AMQP.start("amqp://guest:guest@dev.rabbitmq.com") do |connection, open_ok| AMQP::Channel.new do |channel, open_ok| channel.queue("", :auto_delete => true, :exclusive => true) do |queue, declare_ok| puts "#{queue.name} is ready to go." connection.close { EventMachine.stop { exit } } end end end amqp-1.8.0/examples/guides/queues/12_objects_that_consume_messages.rb0000644000004100000410000000312713321132064026031 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "rubygems" require "amqp" class Consumer # # API # def initialize(channel, queue_name = AMQ::Protocol::EMPTY_STRING) @queue_name = queue_name @channel = channel @channel.on_error(&method(:handle_channel_exception)) end # initialize def start @queue = @channel.queue(@queue_name, :exclusive => true) @queue.subscribe(&method(:handle_message)) end # start # # Implementation # def handle_message(metadata, payload) puts "Received a message: #{payload}, content_type = #{metadata.content_type}" end # handle_message(metadata, payload) def handle_channel_exception(channel, channel_close) puts "Oops... a channel-level exception: code = #{channel_close.reply_code}, message = #{channel_close.reply_text}" end # handle_channel_exception(channel, channel_close) end class Producer # # API # def initialize(channel, exchange) @channel = channel @exchange = exchange end # initialize(channel, exchange) def publish(message, options = {}) @exchange.publish(message, options) end # publish(message, options = {}) end AMQP.start("amqp://guest:guest@dev.rabbitmq.com") do |connection, open_ok| channel = AMQP::Channel.new(connection) worker = Consumer.new(channel, "amqpgem.objects.integration") worker.start producer = Producer.new(channel, channel.default_exchange) puts "Publishing..." producer.publish("Hello, world", :routing_key => "amqpgem.objects.integration") # stop in 2 seconds EventMachine.add_timer(2.0) { connection.close { EventMachine.stop } } end amqp-1.8.0/examples/guides/queues/02a_declaring_a_durable_shared_queue.rb0000644000004100000410000000054713321132064026563 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "rubygems" require "amqp" # Declaring a durable shared queue AMQP.start("amqp://guest:guest@dev.rabbitmq.com") do |connection, open_ok| channel = AMQP::Channel.new(connection) queue = AMQP::Queue.new(channel, "images.resize", :durable => true) connection.close { EventMachine.stop { exit } } end amqp-1.8.0/examples/guides/queues/05_bind_a_queue_using_exchange_name.rb0000644000004100000410000000075713321132064026437 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "rubygems" require "amqp" # Binding a queue to an exchange AMQP.start("amqp://guest:guest@dev.rabbitmq.com") do |connection, open_ok| channel = AMQP::Channel.new(connection) exchange_name = "amq.fanout" channel.queue("", :auto_delete => true, :exclusive => true) do |queue, declare_ok| queue.bind(exchange_name) puts "Bound #{queue.name} to #{exchange_name}" connection.close { EventMachine.stop { exit } } end end amqp-1.8.0/examples/connection/0000755000004100000410000000000013321132064016456 5ustar www-datawww-dataamqp-1.8.0/examples/connection/connect_and_immediately_disconnect.rb0000644000004100000410000000041613321132064026053 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' EventMachine.run do connection = AMQP.connect(:host => '127.0.0.1') connection.disconnect { EventMachine.stop } end amqp-1.8.0/examples/queues/0000755000004100000410000000000013321132064015626 5ustar www-datawww-dataamqp-1.8.0/examples/queues/queue_status.rb0000644000004100000410000000201113321132064020674 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' puts "=> Queue#status example" puts AMQP.start(:host => 'localhost') do |connection| channel = AMQP::Channel.new(connection) queue_name = "amqpgem.integration.queue.status.queue" exchange = channel.fanout("amqpgem.integration.queue.status.fanout", :auto_delete => true) queue = channel.queue(queue_name, :auto_delete => true).bind(exchange) 100.times do |i| print "." exchange.publish(Time.now.to_i.to_s + "_#{i}", :key => queue_name) end $stdout.flush EventMachine.add_timer(0.5) do queue.status do |number_of_messages, number_of_consumers| puts puts "# of messages on status = #{number_of_messages}" puts queue.purge end end show_stopper = Proc.new do $stdout.puts "Stopping..." connection.close { EventMachine.stop } end Signal.trap "INT", show_stopper EventMachine.add_timer(2, show_stopper) end amqp-1.8.0/examples/queues/cancel_default_consumer.rb0000644000004100000410000000200113321132064023010 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' EventMachine.run do connection = AMQP.connect(:host => '127.0.0.1') puts "Connected to RabbitMQ. Running #{AMQP::VERSION} version of the gem..." channel = AMQP::Channel.new(connection) queue = channel.queue("amqpgem.examples.hello_world", :auto_delete => true) exchange = channel.direct("amq.direct") queue.bind(exchange, :routing_key => "amqpgem.key") channel.on_error do |ch, channel_close| puts channel_close.reply_text connection.close { EventMachine.stop } end queue.subscribe do |metadata, payload| puts "Received a message: #{payload}." end EventMachine.add_periodic_timer(1.0) do exchange.publish("Hey, what a great view!", :routing_key => "amqpgem.key") end EventMachine.add_timer(3.0) do queue.unsubscribe do puts "Cancelled default consumer..." connection.close { EventMachine.stop } end end end amqp-1.8.0/examples/queues/declare_a_queue_without_assignment.rb0000644000004100000410000000175613321132064025302 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' puts "=> Queue#initialize example that uses a block" puts AMQP.start("amqp://guest:guest@dev.rabbitmq.com:5672") do |connection, open_ok| AMQP::Channel.new do |channel, open_ok| puts "Channel ##{channel.id} is now open!" AMQP::Queue.new(channel, "", :auto_delete => true) do |queue| puts "#{queue.name} is ready to go" end AMQP::Queue.new(channel, "", :auto_delete => true) do |queue, declare_ok| puts "#{queue.name} is ready to go. AMQP method: #{declare_ok.inspect}" end channel.queue("", :auto_delete => true) do |queue, declare_ok| puts "#{queue.name} is ready to go. AMQP method: #{declare_ok.inspect}" end end show_stopper = Proc.new do $stdout.puts "Stopping..." connection.close { EM.stop { exit } } end Signal.trap "INT", show_stopper EM.add_timer(2, show_stopper) end amqp-1.8.0/examples/queues/basic_get.rb0000755000004100000410000000275313321132064020105 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' if RUBY_VERSION == "1.8.7" class Array alias sample choice end end puts "=> basic.get example" puts AMQP.start(:host => 'localhost') do |connection| channel = AMQP::Channel.new queue_name = "amqpgem.integration.basic.get.queue" expected_number_of_messages = 50 exchange = channel.fanout("amqpgem.integration.basic.get.fanout", :auto_delete => true) queue = channel.queue(queue_name, :auto_delete => true) queue.bind(exchange) do puts "Bound #{exchange.name} => #{queue.name}" end expected_number_of_messages.times do |i| print "." exchange.publish(Time.now.to_i.to_s + "_#{i}", :key => queue_name) end $stdout.flush sleep 1 queue.status do |number_of_messages, number_of_consumers| puts "# of messages on status = #{number_of_messages}" end queue.status do |number_of_messages, number_of_consumers| puts "# of messages on status = #{number_of_messages}" expected_number_of_messages.times do queue.pop do |headers, payload| puts "=> With payload #{payload.inspect}, routing key: #{headers.routing_key}, #{headers.message_count} message(s) left in the queue" end # pop end end show_stopper = Proc.new do $stdout.puts "Stopping..." connection.close { EM.stop { exit } } end Signal.trap "INT", show_stopper EM.add_timer(2, show_stopper) end amqp-1.8.0/examples/queues/declare_and_bind_a_server_named_queue.rb0000644000004100000410000000173113321132064025630 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' puts "=> Queue#initialize example that uses a block" puts AMQP.start("amqp://guest:guest@dev.rabbitmq.com:5672") do |connection, open_ok| AMQP::Channel.new do |channel, open_ok| puts "Channel ##{channel.id} is now open!" if channel.open? xchange = channel.fanout("amq.fanout", :nowait => true) q = AMQP::Queue.new(channel, "", :auto_delete => true) EM.add_timer(0.5) do puts "Channel ##{channel.id} is still open!" if channel.open? q.bind(xchange).subscribe do |header, payload| puts "Got a payload: #{payload}" end EventMachine.add_timer(0.3) { xchange.publish("à bientôt!") } end end show_stopper = Proc.new do $stdout.puts "Stopping..." connection.close { EM.stop { exit } } end Signal.trap "INT", show_stopper EM.add_timer(2, show_stopper) end amqp-1.8.0/examples/queues/rejecting_messages_without_requeueuing.rb0000644000004100000410000000176213321132064026223 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' if RUBY_VERSION == "1.8.7" class Array alias sample choice end end puts "=> Queue#status example" puts AMQP.start(:host => 'localhost') do |connection| channel = AMQP::Channel.new(connection) exchange = channel.fanout("amqpgem.integration.queue.status.fanout", :auto_delete => true) queue = channel.queue("amqpgem.integration.queue.status.queue", :auto_delete => true) queue.bind(exchange).subscribe do |metadata, payload| puts "Rejecting #{payload}" channel.reject(metadata.delivery_tag) end 100.times do |i| print "." exchange.publish(Time.now.to_i.to_s + "_#{i}", :key => queue.name) end $stdout.flush show_stopper = Proc.new do $stdout.puts "Stopping..." connection.close { EventMachine.stop { exit } } end Signal.trap "INT", show_stopper EventMachine.add_timer(2, show_stopper) end amqp-1.8.0/examples/queues/accessing_message_metadata.rb0000644000004100000410000000431613321132064023462 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' EventMachine.run do connection = AMQP.connect(:host => '127.0.0.1') puts "Connected to RabbitMQ. Running #{AMQP::VERSION} version of the gem..." channel = AMQP::Channel.new(connection) queue = channel.queue("amqpgem.examples.hello_world", :auto_delete => true) exchange = channel.direct("amq.direct") queue.bind(exchange, :routing_key => "amqpgem.key") channel.on_error do |ch, channel_close| puts channel_close.reply_text connection.close { EventMachine.stop } end queue.subscribe do |metadata, payload| puts "metadata.routing_key : #{metadata.routing_key}" puts "metadata.content_type: #{metadata.content_type}" puts "metadata.priority : #{metadata.priority}" puts "metadata.headers : #{metadata.headers.inspect}" puts "metadata.timestamp : #{metadata.timestamp.inspect}" puts "metadata.type : #{metadata.type}" puts "metadata.consumer_tag: #{metadata.consumer_tag}" puts "metadata.delivery_tag: #{metadata.delivery_tag}" puts "metadata.redelivered : #{metadata.redelivered?}" puts "metadata.app_id : #{metadata.app_id}" puts "metadata.correlation_id: #{metadata.correlation_id}" puts "metadata.exchange : #{metadata.exchange}" puts puts "Received a message: #{payload}. Disconnecting..." connection.close { EventMachine.stop } end exchange.publish("Hey, what a great view!", :app_id => "amqpgem.example", :priority => 8, :type => "kinda.checkin", :correlation_id => "b907b65a4876fc0d4b12fbdef1b41fb0a9876a94", # headers table keys can be anything :headers => { :coordinates => { :latitude => 59.35, :longitude => 18.066667 }, :participants => 11, :venue => "Stockholm" }, :timestamp => Time.now.to_i, :routing_key => "amqpgem.key") end amqp-1.8.0/examples/queues/automatic_binding_for_default_direct_exchange.rb0000755000004100000410000000250413321132064027405 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' if RUBY_VERSION == "1.8.7" class Array alias sample choice end end puts "=> Default exchange example" puts AMQP.start(:host => 'localhost') do |connection| ch = AMQP::Channel.new(connection) queue1 = ch.queue("queue1").subscribe do |payload| puts "[#{queue1.name}] => #{payload}" end queue2 = ch.queue("queue2").subscribe do |payload| puts "[#{queue2.name}] => #{payload}" end queue3 = ch.queue("queue3").subscribe do |payload| puts "[#{queue3.name}] => #{payload}" end queues = [queue1, queue2, queue3] # Rely on default direct exchange binding, see section 2.1.2.4 Automatic Mode in AMQP 0.9.1 spec. exchange = AMQP::Exchange.default EM.add_periodic_timer(1) do q = queues.sample $stdout.puts "Publishing to default exchange with routing key = #{q.name}..." exchange.publish "Some payload from #{Time.now.to_i}", :routing_key => q.name end show_stopper = Proc.new do queue1.delete queue2.delete queue3.delete $stdout.puts "Stopping..." connection.close { EM.stop { exit } } end Signal.trap "INT", &show_stopper Signal.trap "TERM", &show_stopper EM.add_timer(7, show_stopper) end amqp-1.8.0/examples/queues/using_explicit_acknowledgements.rb0000644000004100000410000000561513321132064024622 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' puts "=> Subscribing for messages using explicit acknowledgements model" puts # this example uses Kernel#sleep and thus we must run EventMachine reactor in # a separate thread, or nothing will be sent/received while we sleep() on the current thread. t = Thread.new { EventMachine.run } sleep(0.5) # open two connections to imitate two apps connection1 = AMQP.connect connection2 = AMQP.connect connection3 = AMQP.connect channel_exception_handler = Proc.new { |ch, channel_close| EventMachine.stop; raise "channel error: #{channel_close.reply_text}" } # open two channels channel1 = AMQP::Channel.new(connection1) channel1.on_error(&channel_exception_handler) # first app will be given up to 3 messages at a time. If it doesn't # ack any messages after it was delivered 3, messages will be routed to # the app #2. channel1.prefetch(3) channel2 = AMQP::Channel.new(connection2) channel2.on_error(&channel_exception_handler) # app #2 processes messages one-by-one and has to send and ack every time channel2.prefetch(1) # app 3 will just publish messages channel3 = AMQP::Channel.new(connection3) channel3.on_error(&channel_exception_handler) exchange = channel3.direct("amq.direct") queue1 = channel1.queue("amqpgem.examples.acknowledgements.explicit", :auto_delete => false) # purge the queue so that we don't get any redeliveries from previous runs queue1.purge queue1.bind(exchange).subscribe(:ack => true) do |metadata, payload| # do some work sleep(0.2) # acknowledge some messages, they will be removed from the queue if rand > 0.5 # FYI: there is a shortcut, metadata.ack channel1.acknowledge(metadata.delivery_tag, false) puts "[consumer1] Got message ##{metadata.headers['i']}, ack-ed" else # some messages are not ack-ed and will remain in the queue for redelivery # when app #1 connection is closed (either properly or due to a crash) puts "[consumer1] Got message ##{metadata.headers['i']}, SKIPPPED" end end queue2 = channel2.queue!("amqpgem.examples.acknowledgements.explicit", :auto_delete => false) queue2.subscribe(:ack => true) do |metadata, payload| metadata.ack # app 2 always acks messages puts "[consumer2] Received #{payload}, redelivered = #{metadata.redelivered}, ack-ed" end # after some time one of the consumers quits/crashes EventMachine.add_timer(4.0) { connection1.close puts "----- Connection 1 is now closed (we pretend that it has crashed) -----" } EventMachine.add_timer(10.0) do # purge the queue so that we don't get any redeliveries on the next run queue2.purge { connection2.close { connection3.close { EventMachine.stop } } } end i = 0 EventMachine.add_periodic_timer(0.8) { 3.times do exchange.publish("Message ##{i}", :headers => { :i => i }) i += 1 end } t.join amqp-1.8.0/examples/hello_world_with_an_empty_string.rb0000644000004100000410000000147713321132064023504 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../lib", __FILE__)) require 'amqp' EventMachine.run do connection = AMQP.connect(:host => '127.0.0.1') puts "Connected to RabbitMQ. Running #{AMQP::VERSION} version of the gem..." connection.on_error do |conn, connection_close| puts "Handling a connection-level exception: #{connection_close.reply_text}" EventMachine.stop end channel = AMQP::Channel.new(connection) queue = channel.queue("amqpgem.examples.hello_world", :auto_delete => true) exchange = channel.direct("") queue.subscribe do |payload| puts "Received a message: #{payload}. Disconnecting..." connection.close { EventMachine.stop } end exchange.publish "", :routing_key => queue.name, :app_id => "Hello world" end amqp-1.8.0/examples/hello_world_with_large_payload.rb0000644000004100000410000013563513321132064023111 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../lib", __FILE__)) require 'amqp' t = Thread.new { EventMachine.run } if defined?(JRUBY_VERSION) # on the JVM, event loop startup takes longer and .next_tick behavior # seem to be a bit different. Blocking current thread for a moment helps. sleep 0.5 end EventMachine.next_tick { connection = AMQP.connect(:host => '127.0.0.1') puts "Connected to RabbitMQ. Running #{AMQP::VERSION} version of the gem..." channel = AMQP::Channel.new(connection) queue = channel.queue("amqpgem.examples.hello_world", :auto_delete => true) exchange = channel.direct("") queue.subscribe do |payload| puts "Received a message #{payload.bytesize} bytes in size.\n\nDisconnecting..." connection.close { EM.stop { exit } } end message = <<-MESSAGE Historical origin In May, 1974, the Institute of Electrical and Electronic Engineers (IEEE) published a paper entitled "A Protocol for Packet Network Interconnection."[1] The paper's authors, Vinton G. Cerf and Bob Kahn, described an internetworking protocol for sharing resources using packet-switching among the nodes. A central control component of this model was the Transmission Control Program that incorporated both connection-oriented links and datagram services between hosts. The monolithic Transmission Control Program was later divided into a modular architecture consisting of the Transmission Control Protocol at the connection-oriented layer and the Internet Protocol at the internetworking (datagram) layer. The model became known informally as TCP/IP, although formally it was henceforth called the Internet Protocol Suite. Network function TCP provides a communication service at an intermediate level between an application program and the Internet Protocol (IP). That is, when an application program desires to send a large chunk of data across the Internet using IP, instead of breaking the data into IP-sized pieces and issuing a series of IP requests, the software can issue a single request to TCP and let TCP handle the IP details. IP works by exchanging pieces of information called packets. A packet is a sequence of octets and consists of a header followed by a body. The header describes the packet's destination and, optionally, the routers to use for forwarding until it arrives at its destination. The body contains the data IP is transmitting. Due to network congestion, traffic load balancing, or other unpredictable network behavior, IP packets can be lost, duplicated, or delivered out of order. TCP detects these problems, requests retransmission of lost data, rearranges out-of-order data, and even helps minimize network congestion to reduce the occurrence of the other problems. Once the TCP receiver has reassembled the sequence of octets originally transmitted, it passes them to the application program. Thus, TCP abstracts the application's communication from the underlying networking details. TCP is utilized extensively by many of the Internet's most popular applications, including the World Wide Web (WWW), E-mail, File Transfer Protocol, Secure Shell, peer-to-peer file sharing, and some streaming media applications. TCP is optimized for accurate delivery rather than timely delivery, and therefore, TCP sometimes incurs relatively long delays (in the order of seconds) while waiting for out-of-order messages or retransmissions of lost messages. It is not particularly suitable for real-time applications such as Voice over IP. For such applications, protocols like the Real-time Transport Protocol (RTP) running over the User Datagram Protocol (UDP) are usually recommended instead.[2] TCP is a reliable stream delivery service that guarantees delivery of a data stream sent from one host to another without duplication or losing data. Since packet transfer is not reliable, a technique known as positive acknowledgment with retransmission is used to guarantee reliability of packet transfers. This fundamental technique requires the receiver to respond with an acknowledgment message as it receives the data. The sender keeps a record of each packet it sends, and waits for acknowledgment before sending the next packet. The sender also keeps a timer from when the packet was sent, and retransmits a packet if the timer expires. The timer is needed in case a packet gets lost or corrupted.[2] TCP consists of a set of rules: for the protocol, that are used with the Internet Protocol, and for the IP, to send data "in a form of message units" between computers over the Internet. At the same time that IP takes care of handling the actual delivery of the data, TCP takes care of keeping track of the individual units of data transmission, called segments, that a message is divided into for efficient routing through the network. For example, when an HTML file is sent from a Web server, the TCP software layer of that server divides the sequence of octets of the file into segments and forwards them individually to the IP software layer (Internet Layer). The Internet Layer encapsulates each TCP segment into an IP packet by adding a header that includes (among other data) the destination IP address. Even though every packet has the same destination address, they can be routed on different paths through the network. When the client program on the destination computer receives them, the TCP layer (Transport Layer) reassembles the individual segments and ensures they are correctly ordered and error free as it streams them to an application. TCP segment structure Transmission Control Protocol accepts data from a data stream, 'segments' it into chunks, and adds a TCP header creating a TCP segment. The TCP segment is then encapsulated into an IP datagram. A TCP segment is "the packet of information that TCP uses to exchange data with its peers." [3] Note that the term TCP packet, though sometimes informally used, is not in line with current terminology, where segment refers to the TCP PDU (Protocol Data Unit), datagram[4] to the IP PDU and frame to the data link layer PDU: Processes transmit data by calling on the TCP and passing buffers of data as arguments. The TCP packages the data from these buffers into segments and calls on the internet module [e.g. IP] to transmit each segment to the destination TCP.[5] A TCP segment consists of a segment header and a data section. The TCP header contains 10 mandatory fields, and an optional extension field (Options, pink background in table). The data section follows the header. Its contents are the payload data carried for the application. The length of the data section is not specified in the TCP segment header. It can be calculated by subtracting the combined length of the TCP header and the encapsulating IP segment header from the total IP segment length (specified in the IP segment header). TCP Header Bit offset 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 0 Source port Destination port 32 Sequence number 64 Acknowledgment number (if ACK set) 96 Data offset Reserved C W R E C E U R G A C K P S H R S T S Y N F I N Window Size 128 Checksum Urgent pointer (if URG set) 160 ... Options (if Data Offset > 5) ... padding Source port (16 bits) – identifies the sending port Destination port (16 bits) – identifies the receiving port Sequence number (32 bits) – has a dual role: If the SYN flag is set (1), then this is the initial sequence number. The sequence number of the actual first data byte and the acknowledged number in the corresponding ACK are then this sequence number plus 1. If the SYN flag is clear (0), then this is the accumulated sequence number of the first data byte of this packet for the current session. Acknowledgment number (32 bits) – if the ACK flag is set then the value of this field is the next sequence number that the receiver is expecting. This acknowledges receipt of all prior bytes (if any). The first ACK sent by each end acknowledges the other end's initial sequence number itself, but no data. Data offset (4 bits) – specifies the size of the TCP header in 32-bit words. The minimum size header is 5 words and the maximum is 15 words thus giving the minimum size of 20 bytes and maximum of 60 bytes, allowing for up to 40 bytes of options in the header. This field gets its name from the fact that it is also the offset from the start of the TCP segment to the actual data. Reserved (4 bits) – for future use and should be set to zero Flags (8 bits) (aka Control bits) – contains 8 1-bit flags CWR (1 bit) – Congestion Window Reduced (CWR) flag is set by the sending host to indicate that it received a TCP segment with the ECE flag set and had responded in congestion control mechanism (added to header by RFC 3168). ECE (1 bit) – ECN-Echo indicates If the SYN flag is set (1), that the TCP peer is ECN capable. If the SYN flag is clear (0), that a packet with Congestion Experienced flag in IP header set is received during normal transmission (added to header by RFC 3168). URG (1 bit) – indicates that the Urgent pointer field is significant ACK (1 bit) – indicates that the Acknowledgment field is significant. All packets after the initial SYN packet sent by the client should have this flag set. PSH (1 bit) – Push function. Asks to push the buffered data to the receiving application. RST (1 bit) – Reset the connection SYN (1 bit) – Synchronize sequence numbers. Only the first packet sent from each end should have this flag set. Some other flags change meaning based on this flag, and some are only valid for when it is set, and others when it is clear. FIN (1 bit) – No more data from sender Window size (16 bits) – the size of the receive window, which specifies the number of bytes (beyond the sequence number in the acknowledgment field) that the receiver is currently willing to receive (see Flow control and Window Scaling) Checksum (16 bits) – The 16-bit checksum field is used for error-checking of the header and data Urgent pointer (16 bits) – if the URG flag is set, then this 16-bit field is an offset from the sequence number indicating the last urgent data byte Options (Variable 0-320 bits, divisible by 32) – The length of this field is determined by the data offset field. Options 0 and 1 are a single byte (8 bits) in length. The remaining options indicate the total length of the option (expressed in bytes) in the second byte. Padding – The TCP header padding is used to ensure that the TCP header ends and data begins on a 32 bit boundary. The padding is composed of zeros.[6] Some options may only be sent when SYN is set; they are indicated below as [SYN]. 0 (8 bits) - End of options list 1 (8 bits) - No operation (NOP, Padding) This may be used to align option fields on 32-bit boundaries for better performance. 2,4,SS (32 bits) - Maximum segment size (see maximum segment size) [SYN] 3,3,S (24 bits) - Window scale (see window scaling for details) [SYN][7] 4,2 (16 bits) - Selective Acknowledgement permitted. [SYN] (See selective acknowledgments for details)[8] 5,N,BBBB,EEEE,... (variable bits, N is either 10, 18, 26, or 34)- Selective ACKnowledgement (SACK)[9] These first two bytes are followed by a list of 1-4 blocks being selectively acknowledged, specified as 32-bit begin/end pointers. 8,10,TTTT,EEEE (80 bits)- Timestamp and echo of previous timestamp (see TCP timestamps for details)[10] 14,3,S (24 bits) - TCP Alternate Checksum Request. [SYN][11] 15,N,... (variable bits) - TCP Alternate Checksum Data. (The remaining options are obsolete, experimental, not yet standardized, or unassigned) Protocol operation A Simplified TCP State Diagram. See TCP EFSM diagram for a more detailed state diagram including the states inside the ESTABLISHED state. TCP protocol operations may be divided into three phases. Connections must be properly established in a multi-step handshake process (connection establishment) before entering the data transfer phase. After data transmission is completed, the connection termination closes established virtual circuits and releases all allocated resources. A TCP connection is managed by an operating system through a programming interface that represents the local end-point for communications, the Internet socket. During the lifetime of a TCP connection it undergoes a series of state changes:[12] LISTENING : In case of a server, waiting for a connection request from any remote client. SYN-SEND : waiting for the remote peer to send back a TCP segment with the SYN and ACK flags set. (usually set by TCP clients) SYN-RECEIVED : waiting for the remote peer to send back an acknowledgment after having sent back a connection acknowledgment to the remote peer. (usually set by TCP servers) ESTABLISHED : the port is ready to receive/send data from/to the remote peer. FIN-WAIT-1 : FIN-WAIT-2 :Indicates that the client is waiting for the servers fin segment ( which indicates the servers application process is ready to close and the server is ready to initiate it's side of the connection termination) CLOSE-WAIT : LAST-ACK : indicates that the server is in the process of sending it's own fin segment ( which indicates the server's application process is ready to close and the server is ready to initiate it's side of the connection termination ) TIME-WAIT : represents waiting for enough time to pass to be sure the remote peer received the acknowledgment of its connection termination request. According to RFC 793 a connection can stay in TIME-WAIT for a maximum of four minutes know as a MSL (maximum segment lifetime). CLOSED : connection is closed Connection establishment To establish a connection, TCP uses a three-way handshake. Before a client attempts to connect with a server, the server must first bind to a port to open it up for connections: this is called a passive open. Once the passive open is established, a client may initiate an active open. To establish a connection, the three-way (or 3-step) handshake occurs: SYN: The active open is performed by the client sending a SYN to the server. It sets the segment's sequence number to a random value A. SYN-ACK: In response, the server replies with a SYN-ACK. The acknowledgment number is set to one more than the received sequence number (A + 1), and the sequence number that the server chooses for the packet is another random number, B. ACK: Finally, the client sends an ACK back to the server. The sequence number is set to the received acknowledgement value i.e. A + 1, and the acknowledgement number is set to one more than the received sequence number i.e. B + 1. At this point, both the client and server have received an acknowledgment of the connection. Resource usage Most implementations allocate an entry in a table that maps a session to a running operating system process. Because TCP packets do not include a session identifier, both endpoints identify the session using the client's address and port. Whenever a packet is received, the TCP implementation must perform a lookup on this table to find the destination process. The number of sessions in the server side is limited only by memory and can grow as new connections arrive, but the client must allocate a random port before sending the first SYN to the server. This port remains allocated during the whole conversation, and effectively limits the number of outgoing connections from each of the client's IP addresses. If an application fails to properly close unrequired connections, a client can run out of resources and become unable to establish new TCP connections, even from other applications. Both endpoints must also allocate space for unacknowledged packets and received (but unread) data. Data transfer There are a few key features that set TCP apart from User Datagram Protocol: Ordered data transfer - the destination host rearranges according to sequence number[2] Retransmission of lost packets - any cumulative stream not acknowledged is retransmitted[2] Error-free data transfer[13] Flow control - limits the rate a sender transfers data to guarantee reliable delivery. The receiver continually hints the sender on how much data can be received (controlled by the sliding window). When the receiving host's buffer fills, the next acknowledgment contains a 0 in the window size, to stop transfer and allow the data in the buffer to be processed.[2] Congestion control [2] Reliable transmission TCP uses a sequence number to identify each segment of data. The sequence number identifies the order of the segments sent from each computer so that the data can be reconstructed in order, regardless of any fragmentation, disordering, or packet loss that may occur during transmission. For every payload segment transmitted the sequence number must be incremented. In the first two steps of the 3-way handshake, both computers exchange an initial sequence number (ISN). This number can be arbitrary, and should in fact be unpredictable to defend against TCP Sequence Prediction Attacks. TCP primarily uses a cumulative acknowledgment scheme, where the receiver sends an acknowledgment signifying that the receiver has received all data preceding the acknowledged sequence number. Essentially, the first byte in a segment's data field is assigned a sequence number, which is inserted in the sequence number field, and the receiver sends an acknowledgment specifying the sequence number of the next segment they expect to receive. For example, if computer A sends 4 segments with a sequence number of 100 (conceptually, the four segments would have a sequence number of 100, 101, 102 and 103 assigned) then the receiver would send back an acknowledgment of 104 since that is the next segment it expects to receive in the next packet. In addition to cumulative acknowledgments, TCP receivers can also send selective acknowledgments to provide further information. If the sender infers that data has been lost in the network, it retransmits the data. Error detection Sequence numbers and acknowledgments cover discarding duplicate packets, retransmission of lost packets, and ordered-data transfer. To assure correctness a checksum field is included (see TCP segment structure for details on checksumming). The TCP checksum is a weak check by modern standards. Data Link Layers with high bit error rates may require additional link error correction/detection capabilities. The weak checksum is partially compensated for by the common use of a CRC or better integrity check at layer 2, below both TCP and IP, such as is used in PPP or the Ethernet frame. However, this does not mean that the 16-bit TCP checksum is redundant: remarkably, introduction of errors in packets between CRC-protected hops is common, but the end-to-end 16-bit TCP checksum catches most of these simple errors.[14] This is the end-to-end principle at work. Flow control TCP uses an end-to-end flow control protocol to avoid having the sender send data too fast for the TCP receiver to receive and process it reliably. Having a mechanism for flow control is essential in an environment where machines of diverse network speeds communicate. For example, if a PC sends data to a hand-held PDA that is slowly processing received data, the PDA must regulate data flow so as not to be overwhelmed.[2] TCP uses a sliding window flow control protocol. In each TCP segment, the receiver specifies in the receive window field the amount of additional received data (in bytes) that it is willing to buffer for the connection. The sending host can send only up to that amount of data before it must wait for an acknowledgment and window update from the receiving host. TCP sequence numbers and receive windows behave very much like a clock. The receive window shifts each time the receiver receives and acknowledges a new segment of data. Once it runs out of sequence numbers, the sequence number loops back to 0. When a receiver advertises a window size of 0, the sender stops sending data and starts the persist timer. The persist timer is used to protect TCP from a deadlock situation that could arise if a subsequent window size update from the receiver is lost, and the sender cannot send more data until receiving a new window size update from the receiver. When the persist timer expires, the TCP sender attempts recovery by sending a small packet so that the receiver responds by sending another acknowledgement containing the new window size. If a receiver is processing incoming data in small increments, it may repeatedly advertise a small receive window. This is referred to as the silly window syndrome, since it is inefficient to send only a few bytes of data in a TCP segment, given the relatively large overhead of the TCP header. TCP senders and receivers typically employ flow control logic to specifically avoid repeatedly sending small segments. The sender-side silly window syndrome avoidance logic is referred to as Nagle's algorithm. Congestion control The final main aspect of TCP is congestion control. TCP uses a number of mechanisms to achieve high performance and avoid congestion collapse, where network performance can fall by several orders of magnitude. These mechanisms control the rate of data entering the network, keeping the data flow below a rate that would trigger collapse. They also yield an approximately max-min fair allocation between flows. Acknowledgments for data sent, or lack of acknowledgments, are used by senders to infer network conditions between the TCP sender and receiver. Coupled with timers, TCP senders and receivers can alter the behavior of the flow of data. This is more generally referred to as congestion control and/or network congestion avoidance. Modern implementations of TCP contain four intertwined algorithms: Slow-start, congestion avoidance, fast retransmit, and fast recovery (RFC 5681). In addition, senders employ a retransmission timeout (RTO) that is based on the estimated round-trip time (or RTT) between the sender and receiver, as well as the variance in this round trip time. The behavior of this timer is specified in RFC 2988. There are subtleties in the estimation of RTT. For example, senders must be careful when calculating RTT samples for retransmitted packets; typically they use Karn's Algorithm or TCP timestamps (see RFC 1323). These individual RTT samples are then averaged over time to create a Smoothed Round Trip Time (SRTT) using Jacobson's algorithm. This SRTT value is what is finally used as the round-trip time estimate. Enhancing TCP to reliably handle loss, minimize errors, manage congestion and go fast in very high-speed environments are ongoing areas of research and standards development. As a result, there are a number of TCP congestion avoidance algorithm variations. Maximum segment size The Maximum segment size (MSS) is the largest amount of data, specified in bytes, that TCP is willing to send in a single segment. For best performance, the MSS should be set small enough to avoid IP fragmentation, which can lead to excessive retransmissions if there is packet loss. To try to accomplish this, typically the MSS is negotiated using the MSS option when the TCP connection is established, in which case it is determined by the maximum transmission unit (MTU) size of the data link layer of the networks to which the sender and receiver are directly attached. Furthermore, TCP senders can use Path MTU discovery to infer the minimum MTU along the network path between the sender and receiver, and use this to dynamically adjust the MSS to avoid IP fragmentation within the network. Strictly speaking, the MSS is not negotiated between the originator and the receiver, because that would imply that both originator and receiver will negotiate and agree upon a single, unified MSS that applies to all communication in both directions of the connection. In fact, two completely independent values of MSS are permitted for the two directions of data flow in a TCP connection.[15] This situation may arise, for example, if one of the devices participating in a connection has an extremely limited amount memory reserved (perhaps even smaller than the overall discovered Path MTU) for processing incoming TCP segments. Selective acknowledgments Relying purely on the cumulative acknowledgment scheme employed by the original TCP protocol can lead to inefficiencies when packets are lost. For example, suppose 10,000 bytes are sent in 10 different TCP packets, and the first packet is lost during transmission. In a pure cumulative acknowledgment protocol, the receiver cannot say that it received bytes 1,000 to 9,999 successfully, but failed to receive the first packet, containing bytes 0 to 999. Thus the sender may then have to resend all 10,000 bytes. To solve this problem TCP employs the selective acknowledgment (SACK) option, defined in RFC 2018, which allows the receiver to acknowledge discontinuous blocks of packets that were received correctly, in addition to the sequence number of the last contiguous byte received successively, as in the basic TCP acknowledgment. The acknowledgement can specify a number of SACK blocks, where each SACK block is conveyed by the starting and ending sequence numbers of a contiguous range that the receiver correctly received. In the example above, the receiver would send SACK with sequence numbers 1,000 and 9,999. The sender thus retransmits only the first packet, bytes 0 to 999. An extension to the SACK option is the "duplicate-SACK" option, defined in RFC 2883. An out-of-order packet delivery can often falsely indicate the TCP sender of lost packet and, in turn, the TCP sender retransmits the suspected-to-be-lost packet and slow down the data delivery to prevent network congestion. The TCP sender undoes the action of slow-down, that is a recovery of the original pace of data transmission, upon receiving a D-SACK that indicates the retransmitted packet is duplicate. The SACK option is not mandatory and it is used only if both parties support it. This is negotiated when connection is established. SACK uses the optional part of the TCP header (see TCP segment structure for details). The use of SACK is widespread - all popular TCP stacks support it. Selective acknowledgment is also used in Stream Control Transmission Protocol (SCTP). Window scaling Main article: TCP window scale option For more efficient use of high bandwidth networks, a larger TCP window size may be used. The TCP window size field controls the flow of data and its value is limited to between 2 and 65,535 bytes. Since the size field cannot be expanded, a scaling factor is used. The TCP window scale option, as defined in RFC 1323, is an option used to increase the maximum window size from 65,535 bytes to 1 Gigabyte. Scaling up to larger window sizes is a part of what is necessary for TCP Tuning. The window scale option is used only during the TCP 3-way handshake. The window scale value represents the number of bits to left-shift the 16-bit window size field. The window scale value can be set from 0 (no shift) to 14 for each direction independently. Both sides must send the option in their SYN segments to enable window scaling in either direction. Some routers and packet firewalls rewrite the window scaling factor during a transmission. This causes sending and receiving sides to assume different TCP window sizes. The result is non-stable traffic that may be very slow. The problem is visible on some sending and receiving sites behind the path of defective routers.[16] TCP timestamps TCP timestamps, defined in RFC 1323, help TCP compute the round-trip time between the sender and receiver. Timestamp options include a 4-byte timestamp value, where the sender inserts its current value of its timestamp clock, and a 4-byte echo reply timestamp value, where the receiver generally inserts the most recent timestamp value that it has received. The sender uses the echo reply timestamp in an acknowledgement to compute the total elapsed time since the acknowledged segment was sent.[2] TCP timestamps are also used to help in the case where TCP sequence numbers encounter their 232 bound and "wrap around" the sequence number space. This scheme is known as Protect Against Wrapped Sequence numbers, or PAWS (see RFC 1323 for details). Furthermore, the Eifel detection algorithm, defined in RFC 3522, which detects unnecessary loss recovery requires TCP timestamps. Out of band data One is able to interrupt or abort the queued stream instead of waiting for the stream to finish. This is done by specifying the data as urgent. This tells the receiving program to process it immediately, along with the rest of the urgent data. When finished, TCP informs the application and resumes back to the stream queue. An example is when TCP is used for a remote login session, the user can send a keyboard sequence that interrupts or aborts the program at the other end. These signals are most often needed when a program on the remote machine fails to operate correctly. The signals must be sent without waiting for the program to finish its current transfer.[2] TCP OOB data was not designed for the modern Internet. The urgent pointer only alters the processing on the remote host and doesn't expedite any processing on the network itself. When it gets to the remote host there are two slightly different interpretations of the protocol, which means only single bytes of OOB data are reliable. This is assuming it's reliable at all as it's one of the least commonly used protocol elements and tends to be poorly implemented. [17][18] Forcing data delivery Normally, TCP waits for the buffer to exceed the maximum segment size before sending any data. This creates serious delays when the two sides of the connection are exchanging short messages and need to receive the response before continuing. For example, the login sequence at the beginning of a telnet session begins with the short message "Login", and the session cannot make any progress until these five characters have been transmitted and the response has been received. This process can be seriously delayed by TCP's normal behavior when the message is provided to TCP in several send calls. However, an application can force delivery of segments to the output stream using a push operation provided by TCP to the application layer.[2] This operation also causes TCP to set the PSH flag or control bit to ensure that data is delivered immediately to the application layer by the receiving transport layer. In the most extreme cases, for example when a user expects each keystroke to be echoed by the receiving application, the push operation can be used each time a keystroke occurs. More generally, application programs use this function to force output to be sent after writing a character or line of characters. By forcing the data to be sent immediately, delays and wait time are reduced. Connection termination The connection termination phase uses, at most, a four-way handshake, with each side of the connection terminating independently. When an endpoint wishes to stop its half of the connection, it transmits a FIN packet, which the other end acknowledges with an ACK. Therefore, a typical tear-down requires a pair of FIN and ACK segments from each TCP endpoint. After both FIN/ACK exchanges are concluded, the terminating side waits for a timeout before finally closing the connection, during which time the local port is unavailable for new connections; this prevents confusion due to delayed packets being delivered during subsequent connections. A connection can be "half-open", in which case one side has terminated its end, but the other has not. The side that has terminated can no longer send any data into the connection, but the other side can. The terminating side should continue reading the data until the other side terminates as well. It is also possible to terminate the connection by a 3-way handshake, when host A sends a FIN and host B replies with a FIN & ACK (merely combines 2 steps into one) and host A replies with an ACK.[19] This is perhaps the most common method. It is possible for both hosts to send FINs simultaneously then both just have to ACK. This could possibly be considered a 2-way handshake since the FIN/ACK sequence is done in parallel for both directions. Some host TCP stacks may implement a "half-duplex" close sequence, as Linux or HP-UX do. If such a host actively closes a connection but still has not read all the incoming data the stack already received from the link, this host sends a RST instead of a FIN (Section 4.2.2.13 in RFC 1122). This allows a TCP application to be sure the remote application has read all the data the former sent—waiting the FIN from the remote side, when it actively closes the connection. However, the remote TCP stack cannot distinguish between a Connection Aborting RST and this Data Loss RST. Both cause the remote stack to throw away all the data it received, but that the application still didn't read.[clarification needed] Some application protocols may violate the OSI model layers, using the TCP open/close handshaking for the application protocol open/close handshaking - these may find the RST problem on active close. As an example: s = connect(remote); send(s, data); close(s); For a usual program flow like above, a TCP/IP stack like that described above does not guarantee that all the data arrives to the other application. Vulnerabilities TCP may be attacked in a variety of ways. The results of a thorough security assessment of the TCP, along with possible mitigations for the identified issues, was published in 2009,[20] and is currently being pursued within the IETF.[21] Denial of service By using a spoofed IP address and repeatedly sending purposely assembled SYN packets, attackers can cause the server to consume large amounts of resources keeping track of the bogus connections. This is known as a SYN flood attack. Proposed solutions to this problem include SYN cookies and Cryptographic puzzles. Sockstress is a similar attack, that might be mitigated with system resource management.[22] An advanced DoS attack involving the exploitation of the TCP Persist Timer was analyzed at Phrack #66.[23] Connection hijacking Main article: TCP sequence prediction attack An attacker who is able to eavesdrop a TCP session and redirect packets can hijack a TCP connection. To do so, the attacker learns the sequence number from the ongoing communication and forges a false segment that looks like the next segment in the stream. Such a simple hijack can result in one packet being erroneously accepted at one end. When the receiving host acknowledges the extra segment to the other side of the connection, synchronization is lost. Hijacking might be combined with ARP or routing attacks that allow taking control of the packet flow, so as to get permanent control of the hijacked TCP connection.[24] Impersonating a different IP address was not difficult prior to RFC 1948, when the initial sequence number was easily guessable. That allowed an attacker to blindly send a sequence of packets that the receiver would believe to come from a different IP address, without the need to deploy ARP or routing attacks: it is enough to ensure that the legitimate host of the impersonated IP address is down, or bring it to that condition using denial of service attacks. This is why the initial sequence number is chosen at random. TCP ports Main article: TCP and UDP port TCP uses the notion of port numbers to identify sending and receiving application end-points on a host, or Internet sockets. Each side of a TCP connection has an associated 16-bit unsigned port number (0-65535) reserved by the sending or receiving application. Arriving TCP data packets are identified as belonging to a specific TCP connection by its sockets, that is, the combination of source host address, source port, destination host address, and destination port. This means that a server computer can provide several clients with several services simultaneously, as long as a client takes care of initiating any simultaneous connections to one destination port from different source ports. Port numbers are categorized into three basic categories: well-known, registered, and dynamic/private. The well-known ports are assigned by the Internet Assigned Numbers Authority (IANA) and are typically used by system-level or root processes. Well-known applications running as servers and passively listening for connections typically use these ports. Some examples include: FTP (21), SSH (22), TELNET (23), SMTP (25) and HTTP (80). Registered ports are typically used by end user applications as ephemeral source ports when contacting servers, but they can also identify named services that have been registered by a third party. Dynamic/private ports can also be used by end user applications, but are less commonly so. Dynamic/private ports do not contain any meaning outside of any particular TCP connection. Development TCP is a complex protocol. However, while significant enhancements have been made and proposed over the years, its most basic operation has not changed significantly since its first specification RFC 675 in 1974, and the v4 specification RFC 793, published in September 1981. RFC 1122, Host Requirements for Internet Hosts, clarified a number of TCP protocol implementation requirements. RFC 2581, TCP Congestion Control, one of the most important TCP-related RFCs in recent years, describes updated algorithms that avoid undue congestion. In 2001, RFC 3168 was written to describe explicit congestion notification (ECN), a congestion avoidance signaling mechanism. The original TCP congestion avoidance algorithm was known as "TCP Tahoe", but many alternative algorithms have since been proposed (including TCP Reno, TCP Vegas, FAST TCP, TCP New Reno, and TCP Hybla). TCP Interactive (iTCP) [25] is a research effort into TCP extensions that allows applications to subscribe to TCP events and register handler components that can launch applications for various purposes, including application-assisted congestion control. Multipath TCP (MPTCP) [26] is an ongoing effort within the IETF that aims at allowing a TCP connection to use multiple paths to maximise resource usage and increase redundancy. The redundancy offered by Multipath TCP in the context of wireless networks [27] enables statistical multiplexing of resources, and thus increases TCP throughput dramatically. TCP Cookie Transactions (TCPCT) is an extension proposed in December 2009 to secure servers against denial-of-service attacks. Unlike SYN cookies, TCPCT does not conflict with other TCP extensions such as window scaling. TCPCT was designed due to necessities of DNSSEC, where servers have to handle large numbers of short-lived TCP connections. tcpcrypt is an extension proposed in July 2010 to provide transport-level encryption directly in TCP itself. It's designed to work transparently and not require any configuration. Unlike TLS (SSL), tcpcrypt itself does not provide authentication, but provides simple primitives down to the application to do that. As of 2010, the first tcpcrypt IETF draft has been published and implementations exist for several major platforms. TCP over wireless networks TCP has been optimized for wired networks. Any packet loss is considered to be the result of network congestion and the congestion window size is reduced dramatically as a precaution. However, wireless links are known to experience sporadic and usually temporary losses due to fading, shadowing, hand off, and other radio effects, that cannot be considered congestion. After the (erroneous) back-off of the congestion window size, due to wireless packet loss, there can be a congestion avoidance phase with a conservative decrease in window size. This causes the radio link to be underutilized. Extensive research has been done on the subject of how to combat these harmful effects. Suggested solutions can be categorized as end-to-end solutions (which require modifications at the client or server),[28] link layer solutions (such as RLP in CDMA2000), or proxy based solutions (which require some changes in the network without modifying end nodes.[28][29] Hardware implementations One way to overcome the processing power requirements of TCP is to build hardware implementations of it, widely known as TCP Offload Engines (TOE). The main problem of TOEs is that they are hard to integrate into computing systems, requiring extensive changes in the operating system of the computer or device. One company to develop such a device was Alacritech. Debugging A packet sniffer, which intercepts TCP traffic on a network link, can be useful in debugging networks, network stacks and applications that use TCP by showing the user what packets are passing through a link. Some networking stacks support the SO_DEBUG socket option, which can be enabled on the socket using setsockopt. That option dumps all the packets, TCP states, and events on that socket, which is helpful in debugging. Netstat is another utility that can be used for debugging. Alternatives For many applications TCP is not appropriate. One big problem (at least with normal implementations) is that the application cannot get at the packets coming after a lost packet until the retransmitted copy of the lost packet is received. This causes problems for real-time applications such as streaming multimedia (such as Internet radio), real-time multiplayer games and voice over IP (VoIP) where it is sometimes more useful to get most of the data in a timely fashion than it is to get all of the data in order. For both historical and performance reasons, most storage area networks (SANs) prefer to use Fibre Channel protocol (FCP) instead of TCP/IP. Also for embedded systems, network booting and servers that serve simple requests from huge numbers of clients (e.g. DNS servers) the complexity of TCP can be a problem. Finally, some tricks such as transmitting data between two hosts that are both behind NAT (using STUN or similar systems) are far simpler without a relatively complex protocol like TCP in the way. Generally, where TCP is unsuitable, the User Datagram Protocol (UDP) is used. This provides the application multiplexing and checksums that TCP does, but does not handle building streams or retransmission, giving the application developer the ability to code them in a way suitable for the situation, or to replace them with other methods like forward error correction or interpolation. SCTP is another IP protocol that provides reliable stream oriented services similar to TCP. It is newer and considerably more complex than TCP, and has not yet seen widespread deployment. However, it is especially designed to be used in situations where reliability and near-real-time considerations are important. Venturi Transport Protocol (VTP) is a patented proprietary protocol that is designed to replace TCP transparently to overcome perceived inefficiencies related to wireless data transport. TCP also has issues in high bandwidth environments. The TCP congestion avoidance algorithm works very well for ad-hoc environments where the data sender is not known in advance, but if the environment is predictable, a timing based protocol such as Asynchronous Transfer Mode (ATM) can avoid TCP's retransmits overhead. Multipurpose Transaction Protocol (MTP/IP) is patented proprietary software that is designed to adaptively achieve high throughput and transaction performance in a wide variety of network conditions, particularly those where TCP is perceived to be inefficient. Checksum computation TCP checksum for IPv4 When TCP runs over IPv4, the method used to compute the checksum is defined in RFC 793: The checksum field is the 16 bit one's complement of the one's complement sum of all 16-bit words in the header and text. If a segment contains an odd number of header and text octets to be checksummed, the last octet is padded on the right with zeros to form a 16-bit word for checksum purposes. The pad is not transmitted as part of the segment. While computing the checksum, the checksum field itself is replaced with zeros. In other words, after appropriate padding, all 16-bit words are added using one's complement arithmetic. The sum is then bitwise complemented and inserted as the checksum field. A pseudo-header that mimics the IPv4 packet header used in the checksum computation is shown in the table below. TCP pseudo-header (IPv4) Bit offset 0–3 4–7 8–15 16–31 0 Source address 32 Destination address 64 Zeros Protocol TCP length 96 Source port Destination port 128 Sequence number 160 Acknowledgement number 192 Data offset Reserved Flags Window 224 Checksum Urgent pointer 256 Options (optional) 256/288+ Data The source and destination addresses are those of the IPv4 header. The protocol value is 6 for TCP (cf. List of IP protocol numbers). The TCP length field is the length of the TCP header and data. TCP checksum for IPv6 When TCP runs over IPv6, the method used to compute the checksum is changed, as per RFC 2460: Any transport or other upper-layer protocol that includes the addresses from the IP header in its checksum computation must be modified for use over IPv6, to include the 128-bit IPv6 addresses instead of 32-bit IPv4 addresses. A pseudo-header that mimics the IPv6 header for computation of the checksum is shown below. TCP pseudo-header (IPv6) Bit offset 0 - 7 8–15 16–23 24–31 0 Source address 32 64 96 128 Destination address 160 192 224 256 TCP length 288 Zeros Next header 320 Source port Destination port 352 Sequence number 384 Acknowledgement number 416 Data offset Reserved Flags Window 448 Checksum Urgent pointer 480 Options (optional) 480/512+ Data Source address – the one in the IPv6 header Destination address – the final destination; if the IPv6 packet doesn't contain a Routing header, TCP uses the destination address in the IPv6 header, otherwise, at the originating node, it uses the address in the last element of the Routing header, and, at the receiving node, it uses the destination address in the IPv6 header. TCP length – the length of the TCP header and data Next Header – the protocol value for TCP Checksum offload Many TCP/IP software stack implementations provide options to use hardware assistance to automatically compute the checksum in the network adapter prior to transmission onto the network or upon reception from the network for validation. MESSAGE payload = message * 87 puts "Publishing a message #{payload.bytesize} bytes in size" exchange.publish(payload, :routing_key => queue.name) } t.join amqp-1.8.0/examples/extensions/0000755000004100000410000000000013321132064016516 5ustar www-datawww-dataamqp-1.8.0/examples/extensions/rabbitmq/0000755000004100000410000000000013321132064020317 5ustar www-datawww-dataamqp-1.8.0/examples/extensions/rabbitmq/using_alternate_exchanges.rb0000644000004100000410000000170313321132064026056 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../../lib", __FILE__)) require 'amqp' EventMachine.run do connection = AMQP.connect(:host => '127.0.0.1') puts "Connecting to RabbitMQ. Running #{AMQP::VERSION} version of the gem..." channel = AMQP::Channel.new(connection) queue = channel.queue("amqpgem.examples.hello_world", :auto_delete => true) exchange1 = channel.fanout("my.fanout1", :auto_delete => true) exchange2 = channel.fanout("my.fanout2", :auto_delete => true, :arguments => { "alternate-exchange" => "my.fanout1" }) queue.bind(exchange1).subscribe do |payload| puts "Received a message: #{payload}. Disconnecting..." connection.close { EventMachine.stop } end exchange1.publish "This message will be routed because of the binding", :mandatory => true exchange2.publish "This message will be re-routed to alternate-exchange", :mandatory => true end amqp-1.8.0/examples/extensions/rabbitmq/publisher_confirmations_with_transient_messages.rb0000644000004100000410000000234713321132064032613 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../../lib", __FILE__)) require 'amqp' require "amqp/extensions/rabbitmq" EventMachine.run do connection = AMQP.connect(:host => '127.0.0.1') puts "Connecting to RabbitMQ. Running #{AMQP::VERSION} version of the gem..." AMQP::Channel.new(connection) do |channel| puts "Channel #{channel.id} is now open" channel.confirm_select channel.on_error { |ch, channel_close| puts "Oops! a channel-level exception: #{channel_close.reply_text}" } channel.on_ack { |basic_ack| puts "Received basic_ack: multiple = #{basic_ack.multiple}, delivery_tag = #{basic_ack.delivery_tag}" } x = channel.fanout("amq.fanout") channel.queue("", :auto_delete => true) do |q| q.bind(x).subscribe(:ack => true) do |header, payload| puts "Received #{payload}" end end EventMachine.add_timer(0.5) do 10.times do |i| puts "Publishing message ##{i + 1}" x.publish("Message ##{i + 1}") end end end show_stopper = Proc.new { connection.close { EventMachine.stop } } EM.add_timer(6, show_stopper) Signal.trap('INT', show_stopper) Signal.trap('TERM', show_stopper) end amqp-1.8.0/examples/extensions/rabbitmq/per_queue_message_ttl.rb0000644000004100000410000000243013321132064025224 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../../lib", __FILE__)) require 'amqp' require "amqp/extensions/rabbitmq" AMQP.start do |connection| puts "Connected!" channel = AMQP::Channel.new(connection) channel.on_error do |ch, channel_close| puts "Oops! a channel-level exception: #{channel_close.reply_text}" end x = channel.fanout("amq.fanout") channel.queue("", :auto_delete => true, :arguments => { "x-message-ttl" => 1000 }) do |q| puts "Declared a new server-named queue: #{q.name}" q.bind(x) EventMachine.add_timer(0.3) do 10.times do |i| puts "Publishing message ##{i}" x.publish("Message ##{i}") end end EventMachine.add_timer(0.7) do q.pop do |headers, payload| puts "Got a message: #{payload}" end end EventMachine.add_timer(1.5) do q.pop do |headers, payload| if payload.nil? puts "No messages in the queue" else raise "x-message-ttl didn't seem to work (timeout isn't up)" end end end end show_stopper = Proc.new { AMQP.stop { EventMachine.stop } } EM.add_timer(3, show_stopper) Signal.trap('INT', show_stopper) Signal.trap('TERM', show_stopper) end amqp-1.8.0/examples/extensions/rabbitmq/connection_blocking.rb0000644000004100000410000000206013321132064024651 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../../lib", __FILE__)) require 'amqp' require "amqp/extensions/rabbitmq" puts "=> Demonstrating connection.blocked" puts # This example requires high memory watermark to be set # really low to demonstrate blocking. # # rabbitmqctl set_vm_memory_high_watermark 0.00000001 # # should do it. EventMachine.run do connection = AMQP.connect(:host => '127.0.0.1') connection.on_blocked do |conn, conn_blocked| puts "Connection blocked, reason: #{conn_blocked.reason}" end connection.on_unblocked do |conn, _| puts "Connection unblocked" end puts "Connecting to RabbitMQ. Running #{AMQP::VERSION} version of the gem..." AMQP::Channel.new(connection) do |ch| x = ch.default_exchange puts "Publishing..." x.publish("z" * 1024 * 1024 * 24) end show_stopper = Proc.new { connection.close { EventMachine.stop } } EM.add_timer(120, show_stopper) Signal.trap('INT', show_stopper) Signal.trap('TERM', show_stopper) end amqp-1.8.0/examples/hello_world.rb0000755000004100000410000000126113321132064017161 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../lib", __FILE__)) require 'amqp' EventMachine.run do connection = AMQP.connect(:host => '127.0.0.1') puts "Connected to RabbitMQ. Running #{AMQP::VERSION} version of the gem..." channel = AMQP::Channel.new(connection) queue = channel.queue("amqpgem.examples.hello_world", :auto_delete => true) exchange = channel.default_exchange queue.subscribe do |payload| puts "Received a message: #{payload}. Disconnecting..." connection.close { EventMachine.stop } end exchange.publish "Hello, world!", :routing_key => queue.name, :app_id => "Hello world" end amqp-1.8.0/examples/channels/0000755000004100000410000000000013321132064016112 5ustar www-datawww-dataamqp-1.8.0/examples/channels/qos_aka_prefetch.rb0000644000004100000410000000116413321132064021737 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' puts "=> Channel#prefetch" puts AMQP.start(:host => 'localhost') do |connection| ch = AMQP::Channel.new ch.on_error do |ch, channel_close| raise "Oops! a channel-level exception: #{channel_close.inspect}" end ch.prefetch(1, false) do |_| puts "qos callback has fired" end show_stopper = Proc.new do $stdout.puts "Stopping..." connection.close { EM.stop { exit } } end Signal.trap "INT", &show_stopper EM.add_timer(1, show_stopper) end amqp-1.8.0/examples/channels/qos_aka_prefetch_without_callback.rb0000644000004100000410000000110413321132064025330 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' puts "=> Channel#prefetch" puts AMQP.start(:host => 'localhost') do |connection| ch = AMQP::Channel.new ch.on_error do |ch, channel_close| raise "Oops! a channel-level exception: #{channel_close.inspect}" end ch.prefetch(1, false) show_stopper = Proc.new do $stdout.puts "Stopping..." connection.close { EM.stop { exit } } end Signal.trap "INT", show_stopper EM.add_timer(1, show_stopper) end amqp-1.8.0/examples/channels/open_channel_without_assignment.rb0000644000004100000410000000103513321132064025102 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' puts "=> Channel#initialize example that uses a block" puts AMQP.start(:host => 'localhost') do |connection| AMQP::Channel.new do |channel, open_ok| puts "Channel ##{channel.id} is now open!" end show_stopper = Proc.new do $stdout.puts "Stopping..." connection.close { EM.stop { exit } } end Signal.trap "INT", show_stopper EM.add_timer(2, show_stopper) end amqp-1.8.0/examples/channels/prefetch_as_constructor_argument.rb0000644000004100000410000000111513321132064025267 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' puts "=> Channel#prefetch" puts AMQP.start(:host => 'localhost') do |connection| ch = AMQP::Channel.new(connection, AMQP::Channel.next_channel_id, :prefetch => 1) ch.on_error do |ex| raise "Oops! there has been a channel-level exception" end show_stopper = Proc.new do $stdout.puts "Stopping..." connection.close { EM.stop { exit } } end Signal.trap "INT", show_stopper EM.add_timer(1, show_stopper) end amqp-1.8.0/examples/channels/open_channel_with_one_arity_callback.rb0000644000004100000410000000102413321132064026005 0ustar www-datawww-data#!/usr/bin/env ruby # encoding: utf-8 require "bundler" Bundler.setup $:.unshift(File.expand_path("../../../lib", __FILE__)) require 'amqp' puts "=> Channel#initialize example that uses a block" puts AMQP.start(:host => 'localhost') do |connection| AMQP::Channel.new do |channel| puts "Channel ##{channel.id} is now open!" end show_stopper = Proc.new do $stdout.puts "Stopping..." connection.close { EM.stop { exit } } end Signal.trap "INT", show_stopper EM.add_timer(2, show_stopper) end amqp-1.8.0/Rakefile0000644000004100000410000000225313321132064014150 0ustar www-datawww-datarequire 'fileutils' require 'rspec/core/rake_task' desc "Run spec suite (uses Rspec2)" RSpec::Core::RakeTask.new(:spec) { |t|} namespace :spec do desc "Clean up rbx compiled files and run spec suite" RSpec::Core::RakeTask.new(:ci) { |t| Dir.glob("**/*.rbc").each {|f| FileUtils.rm_f(f) } } end desc "Run specs with RCov" RSpec::Core::RakeTask.new(:rcov) do |t| t.rcov = true t.rcov_opts = ['--exclude', 'spec'] end desc "Generate AMQP specification classes" task :codegen do sh 'ruby protocol/codegen.rb > lib/amqp/spec.rb' sh 'ruby lib/amqp/spec.rb' end desc "Build the gem" task :gem do sh 'gem build *.gemspec' end desc "Synonym for gem" task :pkg => :gem desc "Synonym for gem" task :package => :gem desc "Regenerate contributors file." task :contributors do authors = %x{git log | grep ^Author:}.split("\n") results = authors.reduce(Hash.new) do |results, line| name = line.sub(/^Author: (.+) <.+>$/, '\1') results[name] ||= 0 results[name] += 1 results end results = results.sort_by { |_, count| count }.reverse File.open("CONTRIBUTORS", "w") do |file| results.each do |name, count| file.puts "#{name}: #{count}" end end end amqp-1.8.0/lib/0000755000004100000410000000000013321132064013247 5ustar www-datawww-dataamqp-1.8.0/lib/amqp/0000755000004100000410000000000013321132064014205 5ustar www-datawww-dataamqp-1.8.0/lib/amqp/session.rb0000644000004100000410000011065313321132064016223 0ustar www-datawww-data# encoding: utf-8 require "eventmachine" require "amqp/framing/string/frame" require "amqp/auth_mechanism_adapter" require "amqp/broker" require "amqp/channel" require "amqp/channel_id_allocator" require "amq/settings" module AMQP # AMQP session represents connection to the broker. Session objects let you define callbacks for # various TCP connection lifecycle events, for instance: # # * Connection is established # * Connection has failed # * Authentication has failed # * Connection is lost (there is a network failure) # * AMQP connection is opened # * AMQP connection parameters (tuning) are negotiated and accepted by the broker # * AMQP connection is properly closed # # h2. Key methods # # * {Session#on_connection} # * {Session#on_open} # * {Session#on_disconnection} # * {Session#on_possible_authentication_failure} # * {Session#on_tcp_connection_failure} # * {Session#on_tcp_connection_loss} # * {Session#reconnect} # * {Session#connected?} # # # @api public class Session < EM::Connection include AMQP::ChannelIdAllocator # # Behaviours # include Openable include Callbacks extend ProtocolMethodHandlers extend RegisterEntityMixin register_entity :channel, AMQP::Channel # # API # attr_accessor :logger attr_accessor :settings # @return [Array<#call>] attr_reader :callbacks # The locale defines the language in which the server will send reply texts. # # @see https://www.rabbitmq.com/resources/specs/amqp-xml-doc0-9-1.pdf AMQP 0.9.1 protocol reference (Section 1.4.2.2) attr_accessor :locale # Client capabilities # # @see https://www.rabbitmq.com/resources/specs/amqp-xml-doc0-9-1.pdf AMQP 0.9.1 protocol reference (Section 1.4.2.2.1) attr_accessor :client_properties # Authentication mechanism used. # # @see https://www.rabbitmq.com/resources/specs/amqp-xml-doc0-9-1.pdf AMQP 0.9.1 protocol reference (Section 1.4.2.2) attr_reader :mechanism # Authentication mechanisms broker supports. # # @see https://www.rabbitmq.com/resources/specs/amqp-xml-doc0-9-1.pdf AMQP 0.9.1 protocol reference (Section 1.4.2.2) attr_reader :server_authentication_mechanisms # Channels within this connection. # # @see http://bit.ly/amqp091spec AMQP 0.9.1 specification (Section 2.2.5) attr_reader :channels # Maximum channel number that the server permits this connection to use. # Usable channel numbers are in the range 1..channel_max. # Zero indicates no specified limit. # # @see https://www.rabbitmq.com/resources/specs/amqp-xml-doc0-9-1.pdf AMQP 0.9.1 protocol reference (Sections 1.4.2.5.1 and 1.4.2.6.1) attr_accessor :channel_max # Maximum frame size that the server permits this connection to use. # # @see https://www.rabbitmq.com/resources/specs/amqp-xml-doc0-9-1.pdf AMQP 0.9.1 protocol reference (Sections 1.4.2.5.2 and 1.4.2.6.2) attr_accessor :frame_max attr_accessor :connection_timeout attr_reader :known_hosts class << self # Settings def settings @settings ||= AMQ::Settings.default end def logger @logger ||= begin require "logger" Logger.new(STDERR) end end def logger=(logger) methods = AMQP::Logging::REQUIRED_METHODS unless methods.all? { |method| logger.respond_to?(method) } raise AMQP::Logging::IncompatibleLoggerError.new(methods) end @logger = logger end # @return [Boolean] Current value of logging flag. def logging settings[:logging] end # Turns loggin on or off. def logging=(boolean) settings[:logging] = boolean end end # @group Connecting, reconnecting, disconnecting def initialize(*args, &block) super(*args) connection_options_or_string = args.first other_options = args[1] || {} self.logger = self.class.logger # channel => collected frames. MK. @frames = Hash.new { Array.new } @channels = Hash.new @callbacks = Hash.new opening! # track TCP connection state, used to detect initial TCP connection failures. @tcp_connection_established = false @tcp_connection_failed = false @intentionally_closing_connection = false @settings = AMQ::Settings.configure(connection_options_or_string).merge(other_options) @on_tcp_connection_failure = Proc.new { |settings| closed! if cb = @settings[:on_tcp_connection_failure] cb.call(settings) else raise self.class.tcp_connection_failure_exception_class.new(settings) end } @on_possible_authentication_failure = @settings[:on_possible_authentication_failure] || Proc.new { |settings| raise self.class.authentication_failure_exception_class.new(settings) } @mechanism = normalize_auth_mechanism(@settings.fetch(:auth_mechanism, "PLAIN")) @locale = @settings.fetch(:locale, "en_GB") @client_properties = Settings.client_properties.merge(@settings.fetch(:client_properties, Hash.new)) @auto_recovery = (!!@settings[:auto_recovery]) @connection_timeout = (@settings[:timeout] || @settings[:connection_timeout] || 3).to_f self.reset self.set_pending_connect_timeout(@connection_timeout) unless defined?(JRUBY_VERSION) end # initialize # @return [Boolean] true if this AMQP connection is currently open # @api plugin def connected? self.opened? end # @return [String] Broker hostname this connection uses # @api public def hostname @settings[:host] end alias host hostname # @return [String] Broker port this connection uses # @api public def port @settings[:port] end # @return [String] Broker endpoint in the form of HOST:PORT/VHOST # @api public def broker_endpoint "#{self.hostname}:#{self.port}/#{self.vhost}" end # @return [String] Username used by this connection # @api public def username @settings[:user] end # username alias user username # Properly close connection with AMQ broker, as described in # section 2.2.4 of the {https://www.rabbitmq.com/resources/specs/amqp0-9-1.pdf AMQP 0.9.1 specification}. # # @api plugin # @see #close_connection def disconnect(reply_code = 200, reply_text = "Goodbye", &block) @intentionally_closing_connection = true self.on_disconnection do @frames.clear block.call if block end # ruby-amqp/amqp#66, MK. if self.open? closing! self.send_frame(AMQ::Protocol::Connection::Close.encode(reply_code, reply_text, 0, 0)) elsif self.closing? # no-op else self.disconnection_successful end end alias close disconnect # @endgroup # @group Broker information # Server properties (product information, platform, et cetera) # # @return [Hash] # @see https://www.rabbitmq.com/resources/specs/amqp-xml-doc0-9-1.pdf AMQP 0.9.1 protocol documentation (Section 1.4.2.1.3) attr_reader :server_properties # Server capabilities (usually used to detect AMQP 0.9.1 extensions like basic.nack, publisher # confirms and so on) # # @return [Hash] # @see https://www.rabbitmq.com/resources/specs/amqp-xml-doc0-9-1.pdf AMQP 0.9.1 protocol documentation (Section 1.4.2.1.3) attr_reader :server_capabilities # Locales server supports # # @see https://www.rabbitmq.com/resources/specs/amqp-xml-doc0-9-1.pdf AMQP 0.9.1 protocol documentation (Section 1.4.2.1.3) attr_reader :server_locales # @return [AMQP::Broker] Broker this connection is established with def broker @broker ||= AMQP::Broker.new(@server_properties) end # broker # @endgroup # Defines a callback that will be executed when AMQP connection is considered open: # after client and broker has agreed on max channel identifier and maximum allowed frame # size and authentication succeeds. You can define more than one callback. # # @see #on_closed # @api public def on_open(&block) @connection_deferrable.callback(&block) end alias on_connection on_open # @group Error Handling and Recovery # Defines a callback that will be run when broker confirms connection termination # (client receives connection.close-ok). You can define more than one callback. # # @see #on_closed # @api public def on_closed(&block) @disconnection_deferrable.callback(&block) end alias on_disconnection on_closed # Defines a callback that will be run when initial TCP connection fails. # You can define only one callback. # # @api public def on_tcp_connection_failure(&block) @on_tcp_connection_failure = block end # Defines a callback that will be run when TCP connection to AMQP broker is lost (interrupted). # You can define only one callback. # # @api public def on_tcp_connection_loss(&block) @on_tcp_connection_loss = block end # Defines a callback that will be run when TCP connection is closed before authentication # finishes. Usually this means authentication failure. You can define only one callback. # # @api public def on_possible_authentication_failure(&block) @on_possible_authentication_failure = block end # Defines a callback that will be executed after TCP connection is interrupted (typically because of a network failure). # Only one callback can be defined (the one defined last replaces previously added ones). # # @api public def on_connection_interruption(&block) self.redefine_callback(:after_connection_interruption, &block) end alias after_connection_interruption on_connection_interruption # Defines a callback that will be executed when connection is closed after # connection-level exception. Only one callback can be defined (the one defined last # replaces previously added ones). # # @api public def on_error(&block) self.redefine_callback(:error, &block) end # Defines a callback that will be executed after TCP connection has recovered after a network failure # but before AMQP connection is re-opened. # Only one callback can be defined (the one defined last replaces previously added ones). # # @api public def before_recovery(&block) self.redefine_callback(:before_recovery, &block) end # Defines a callback that will be executed after AMQP connection has recovered after a network failure.. # Only one callback can be defined (the one defined last replaces previously added ones). # # @api public def on_recovery(&block) self.redefine_callback(:after_recovery, &block) end alias after_recovery on_recovery # @return [Boolean] whether connection is in the automatic recovery mode # @api public def auto_recovering? !!@auto_recovery end alias auto_recovery? auto_recovering? # Performs recovery of channels that are in the automatic recovery mode. # # @see Channel#auto_recover # @see Queue#auto_recover # @see Exchange#auto_recover # @api plugin def auto_recover @channels.select { |channel_id, ch| ch.auto_recovering? }.each { |n, ch| ch.auto_recover } end # auto_recover # @endgroup # @group Blocked connection notifications def on_blocked(&fn) @on_blocked = fn end def on_unblocked(&fn) @on_unblocked = fn end # @endgroup # # Implementation # # Overrides TCP connection failure exception to one that inherits from AMQP::Error # and thus is backwards compatible. # # @private # @api plugin # @return [Class] AMQP::TCPConnectionFailed def self.tcp_connection_failure_exception_class @tcp_connection_failure_exception_class ||= AMQP::TCPConnectionFailed end # self.tcp_connection_failure_exception_class # Overrides authentication failure exception to one that inherits from AMQP::Error # and thus is backwards compatible. # # @private # @api plugin # @return [Class] AMQP::PossibleAuthenticationFailureError def self.authentication_failure_exception_class @authentication_failure_exception_class ||= AMQP::PossibleAuthenticationFailureError end # self.authentication_failure_exception_class # @group Connection operations # Initiates connection to AMQP broker. If callback is given, runs it when (and if) AMQP connection # succeeds. # # @option settings [String] :host ("127.0.0.1") Hostname AMQ broker runs on. # @option settings [String] :port (5672) Port AMQ broker listens on. # @option settings [String] :vhost ("/") Virtual host to use. # @option settings [String] :user ("guest") Username to use for authentication. # @option settings [String] :pass ("guest") Password to use for authentication. # @option settings [String] :auth_mechanism ("PLAIN") SASL authentication mechanism to use. # @option settings [String] :ssl (false) Should be use TLS (SSL) for connection? # @option settings [String] :timeout (nil) Connection timeout. # @option settings [Fixnum] :heartbeat (0) Connection heartbeat, in seconds. # @option settings [Fixnum] :frame_max (131072) Maximum frame size to use. If broker cannot support frames this large, broker's maximum value will be used instead. # # @param [Hash] settings # def self.connect(settings = {}, &block) def self.connect(connection_string_or_opts = ENV['RABBITMQ_URL'], other_options = {}, &block) @settings = AMQ::Settings.configure(connection_string_or_opts).merge(other_options) instance = EventMachine.connect(@settings[:host], @settings[:port], self, @settings) instance.register_connection_callback(&block) instance end # Reconnect after a period of wait. # # @param [Fixnum] period Period of time, in seconds, to wait before reconnection attempt. # @param [Boolean] force If true, enforces immediate reconnection. # @api public def reconnect(force = false, period = 5) if @reconnecting and not force EventMachine::Timer.new(period) { reconnect(true, period) } return end if !@reconnecting @reconnecting = true self.reset end EventMachine.reconnect(@settings[:host], @settings[:port], self) end # Similar to #reconnect, but uses different connection settings # @see #reconnect # @api public def reconnect_to(connection_string_or_options, period = 5) settings = AMQ::Settings.configure(connection_string_or_opts) if !@reconnecting @reconnecting = true self.reset end @settings = Settings.configure(settings) EventMachine.reconnect(@settings[:host], @settings[:port], self) end # Periodically try to reconnect. # # @param [Fixnum] period Period of time, in seconds, to wait before reconnection attempt. # @param [Boolean] force If true, enforces immediate reconnection. # @api public def periodically_reconnect(period = 5) @reconnecting = true self.reset @periodic_reconnection_timer = EventMachine::PeriodicTimer.new(period) { EventMachine.reconnect(@settings[:host], @settings[:port], self) } end # @endgroup # @see #on_open # @private def register_connection_callback(&block) unless block.nil? # delay calling block we were given till after we receive # connection.open-ok. Connection will notify us when # that happens. self.on_open do block.call(self) end end end alias close disconnect # Whether we are in authentication state (after TCP connection was estabilished # but before broker authenticated us). # # @return [Boolean] # @api public def authenticating? @authenticating end # authenticating? # IS TCP connection estabilished and currently active? # @return [Boolean] # @api public def tcp_connection_established? @tcp_connection_established end # tcp_connection_established? # # Implementation # # Backwards compatibility with 0.7.0.a25. MK. Deferrable = EventMachine::DefaultDeferrable alias send_raw send_data # EventMachine reactor callback. Is run when TCP connection is estabilished # but before resumption of the network loop. Note that this includes cases # when TCP connection has failed. # @private def post_init reset # note that upgrading to TLS in #connection_completed causes # Erlang SSL app that RabbitMQ relies on to report # error on TCP connection <0.1465.0>:{ssl_upgrade_error,"record overflow"} # and close TCP connection down. Investigation of this issue is likely # to take some time and to not be worth in as long as #post_init # works fine. MK. upgrade_to_tls_if_necessary rescue Exception => error raise error end # post_init # Called by EventMachine reactor once TCP connection is successfully estabilished. # @private def connection_completed # we only can safely set this value here because EventMachine is a lovely piece of # software that calls #post_init before #unbind even when TCP connection # fails. MK. @tcp_connection_established = true @periodic_reconnection_timer.cancel if @periodic_reconnection_timer # again, this is because #unbind is called in different situations # and there is no easy way to tell initial connection failure # from connection loss. Not in EventMachine 0.12.x, anyway. MK. if @had_successfully_connected_before @recovered = true self.start_automatic_recovery self.upgrade_to_tls_if_necessary end # now we can set it. MK. @had_successfully_connected_before = true @reconnecting = false @handling_skipped_heartbeats = false @last_server_heartbeat = Time.now self.handshake end # @private def close_connection(*args) @intentionally_closing_connection = true super(*args) end # Called by EventMachine reactor when # # * We close TCP connection down # * Our peer closes TCP connection down # * There is a network connection issue # * Initial TCP connection fails # @private def unbind(exception = nil) if !@tcp_connection_established && !@had_successfully_connected_before && !@intentionally_closing_connection @tcp_connection_failed = true logger.error "[amqp] Detected TCP connection failure: #{exception}" self.tcp_connection_failed end closing! @tcp_connection_established = false self.handle_connection_interruption if @reconnecting @disconnection_deferrable.succeed closed! self.tcp_connection_lost if !@intentionally_closing_connection && @had_successfully_connected_before # since AMQP spec dictates that authentication failure is a protocol exception # and protocol exceptions result in connection closure, check whether we are # in the authentication stage. If so, it is likely to signal an authentication # issue. Java client behaves the same way. MK. if authenticating? && !@intentionally_closing_connection @on_possible_authentication_failure.call(@settings) if @on_possible_authentication_failure end end # unbind # # EventMachine receives data in chunks, sometimes those chunks are smaller # than the size of AMQP frame. That's why you need to add some kind of buffer. # # @private def receive_data(chunk) @chunk_buffer << chunk while frame = get_next_frame self.receive_frame(AMQP::Framing::String::Frame.decode(frame)) end end # Called by AMQP::Connection after we receive connection.open-ok. # @api public def connection_successful @authenticating = false opened! @connection_deferrable.succeed end # connection_successful # Called by AMQP::Connection after we receive connection.close-ok. # # @api public def disconnection_successful @disconnection_deferrable.succeed # true for "after writing buffered data" self.close_connection(true) self.reset closed! end # disconnection_successful # Called when time since last server heartbeat received is greater or equal to the # heartbeat interval set via :heartbeat_interval option on connection. # # @api plugin def handle_skipped_heartbeats if !@handling_skipped_heartbeats && @tcp_connection_established && !@intentionally_closing_connection @handling_skipped_heartbeats = true self.cancel_heartbeat_sender self.run_skipped_heartbeats_callbacks end end # @private def initialize_heartbeat_sender @last_server_heartbeat = Time.now @heartbeats_timer = EventMachine::PeriodicTimer.new(self.heartbeat_interval, &method(:send_heartbeat)) end # @private def cancel_heartbeat_sender @heartbeats_timer.cancel if @heartbeats_timer end # Sends AMQ protocol header (also known as preamble). # # @note This must be implemented by all AMQP clients. # @api plugin # @see http://bit.ly/amqp091spec AMQP 0.9.1 specification (Section 2.2) def send_preamble self.send_raw(AMQ::Protocol::PREAMBLE) end # Sends frame to the peer, checking that connection is open. # # @raise [ConnectionClosedError] def send_frame(frame) if closed? raise ConnectionClosedError.new(frame) else self.send_raw(frame.encode) end end # Sends multiple frames, one by one. For thread safety this method takes a channel # object and synchronizes on it. # # @api public def send_frameset(frames, channel) # some (many) developers end up sharing channels between threads and when multiple # threads publish on the same channel aggressively, at some point frames will be # delivered out of order and broker will raise 505 UNEXPECTED_FRAME exception. # If we synchronize on the channel, however, this is both thread safe and pretty fine-grained # locking. Note that "single frame" methods do not need this kind of synchronization. MK. channel.synchronize do frames.each { |frame| self.send_frame(frame) } end end # send_frameset(frames) # Returns heartbeat interval this client uses, in seconds. # This value may or may not be used depending on broker capabilities. # Zero means the server does not want a heartbeat. # # @return [Fixnum] Heartbeat interval this client uses, in seconds. # @see https://www.rabbitmq.com/resources/specs/amqp-xml-doc0-9-1.pdf AMQP 0.9.1 protocol reference (Section 1.4.2.6) def heartbeat_interval @heartbeat_interval end # heartbeat_interval # Returns true if heartbeats are enabled (heartbeat interval is greater than 0) # @return [Boolean] def heartbeats_enabled? @heartbeat_interval && (@heartbeat_interval > 0) end # vhost this connection uses. Default is "/", a historically estabilished convention # of RabbitMQ and amqp gem. # # @return [String] vhost this connection uses # @api public def vhost @settings.fetch(:vhost, "/") end # vhost # @group Error Handling and Recovery # Called when initial TCP connection fails. # @api public def tcp_connection_failed @recovered = false @on_tcp_connection_failure.call(@settings) if @on_tcp_connection_failure end # Called when previously established TCP connection fails. # @api public def tcp_connection_lost @recovered = false @on_tcp_connection_loss.call(self, @settings) if @on_tcp_connection_loss self.handle_connection_interruption end # @return [Boolean] def reconnecting? @reconnecting end # reconnecting? # @private # @api plugin def handle_connection_interruption self.cancel_heartbeat_sender @channels.each { |n, c| c.handle_connection_interruption } self.exec_callback_yielding_self(:after_connection_interruption) end # @private def run_before_recovery_callbacks self.exec_callback_yielding_self(:before_recovery, @settings) @channels.each { |n, ch| ch.run_before_recovery_callbacks } end # @private def run_after_recovery_callbacks self.exec_callback_yielding_self(:after_recovery, @settings) @channels.each { |n, ch| ch.run_after_recovery_callbacks } end # Performs recovery of channels that are in the automatic recovery mode. "before recovery" callbacks # are run immediately, "after recovery" callbacks are run after AMQP connection is re-established and # auto recovery is performed (using #auto_recover). # # Use this method if you want to run automatic recovery process after handling a connection-level exception, # for example, 320 CONNECTION_FORCED (used by RabbitMQ when it is shut down gracefully). # # @see Channel#auto_recover # @see Queue#auto_recover # @see Exchange#auto_recover # @api plugin def start_automatic_recovery self.run_before_recovery_callbacks self.register_connection_callback do # always run automatic recovery, because it is per-channel # and connection has to start it. Channels that did not opt-in for # autorecovery won't be selected. MK. self.auto_recover self.run_after_recovery_callbacks end end # start_automatic_recovery # Defines a callback that will be executed after time since last broker heartbeat is greater # than or equal to the heartbeat interval (skipped heartbeat is detected). # Only one callback can be defined (the one defined last replaces previously added ones). # # @api public def on_skipped_heartbeats(&block) self.redefine_callback(:skipped_heartbeats, &block) end # on_skipped_heartbeats(&block) # @private def run_skipped_heartbeats_callbacks self.exec_callback_yielding_self(:skipped_heartbeats, @settings) end # @endgroup # # Implementation # # Sends connection preamble to the broker. # @api plugin def handshake @authenticating = true self.send_preamble end # Sends connection.open to the server. # # @api plugin # @see https://www.rabbitmq.com/resources/specs/amqp-xml-doc0-9-1.pdf AMQP 0.9.1 protocol reference (Section 1.4.2.7) def open(vhost = "/") self.send_frame(AMQ::Protocol::Connection::Open.encode(vhost)) end # Resets connection state. # # @api plugin def reset_state! # no-op by default end # reset_state! # @api plugin # @see http://tools.ietf.org/rfc/rfc2595.txt RFC 2595 def encode_credentials(username, password) auth_mechanism_adapter.encode_credentials(username, password) end # encode_credentials(username, password) # Retrieves an AuthMechanismAdapter that will encode credentials for # this Adapter. # # @api plugin def auth_mechanism_adapter @auth_mechanism_adapter ||= AuthMechanismAdapter.for_adapter(self) end # Processes a single frame. # # @param [AMQ::Protocol::Frame] frame # @api plugin def receive_frame(frame) @frames[frame.channel] ||= Array.new @frames[frame.channel] << frame if frameset_complete?(@frames[frame.channel]) begin receive_frameset(@frames[frame.channel]) ensure # Ensure that frames always will be cleared # for channel.close, frame.channel will be nil. MK. clear_frames_on(frame.channel) if @frames[frame.channel] end end end # Processes a frameset by finding and invoking a suitable handler. # Heartbeat frames are treated in a special way: they simply update @last_server_heartbeat # value. # # @param [Array] frames # @api plugin def receive_frameset(frames) if self.heartbeats_enabled? # treat incoming traffic as heartbeats. # this operation is pretty expensive under heavy traffic but heartbeats can be disabled # (and are also disabled by default). MK. @last_server_heartbeat = Time.now end frame = frames.first if AMQ::Protocol::HeartbeatFrame === frame # no-op else if callable = AMQP::HandlersRegistry.find(frame.method_class) f = frames.shift callable.call(self, f, frames) else raise MissingHandlerError.new(frames.first) end end end # Clears frames that were received but not processed on given channel. Needs to be called # when the channel is closed. # @private def clear_frames_on(channel_id) raise ArgumentError, "channel id cannot be nil!" if channel_id.nil? @frames[channel_id].clear end # Sends a heartbeat frame if connection is open. # @api plugin def send_heartbeat if tcp_connection_established? && !@handling_skipped_heartbeats && @last_server_heartbeat if @last_server_heartbeat < (Time.now - (self.heartbeat_interval * 2)) && !reconnecting? logger.error "[amqp] Detected missing server heartbeats" self.handle_skipped_heartbeats end send_frame(AMQ::Protocol::HeartbeatFrame) end end # send_heartbeat # Handles connection.start. # # @api plugin # @see https://www.rabbitmq.com/resources/specs/amqp-xml-doc0-9-1.pdf AMQP 0.9.1 protocol reference (Section 1.4.2.1.) def handle_start(connection_start) @server_properties = connection_start.server_properties @server_capabilities = @server_properties["capabilities"] @server_authentication_mechanisms = (connection_start.mechanisms || "").split(" ") @server_locales = Array(connection_start.locales) username = @settings[:user] || @settings[:username] password = @settings[:pass] || @settings[:password] # It's not clear whether we should transition to :opening state here # or in #open but in case authentication fails, it would be strange to have # @status undefined. So lets do this. MK. opening! self.send_frame(AMQ::Protocol::Connection::StartOk.encode(@client_properties, mechanism, self.encode_credentials(username, password), @locale)) end # Handles Connection.Tune-Ok. # # @api plugin # @see https://www.rabbitmq.com/resources/specs/amqp-xml-doc0-9-1.pdf AMQP 0.9.1 protocol reference (Section 1.4.2.6) def handle_tune(connection_tune) @channel_max = connection_tune.channel_max.freeze @frame_max = connection_tune.frame_max.freeze client_heartbeat = @settings[:heartbeat] || @settings[:heartbeat_interval] || 0 @heartbeat_interval = negotiate_heartbeat_value(client_heartbeat, connection_tune.heartbeat) self.send_frame(AMQ::Protocol::Connection::TuneOk.encode(@channel_max, [settings[:frame_max], @frame_max].min, @heartbeat_interval)) self.initialize_heartbeat_sender if heartbeats_enabled? end # handle_tune(method) # Handles Connection.Open-Ok. # # @api plugin # @see https://www.rabbitmq.com/resources/specs/amqp-xml-doc0-9-1.pdf AMQP 0.9.1 protocol reference (Section 1.4.2.8.) def handle_open_ok(open_ok) @known_hosts = open_ok.known_hosts.dup.freeze opened! self.connection_successful if self.respond_to?(:connection_successful) end # Handles connection.close. When broker detects a connection level exception, this method is called. # # @api plugin # @see https://www.rabbitmq.com/resources/specs/amqp-xml-doc0-9-1.pdf AMQP 0.9.1 protocol reference (Section 1.5.2.9) def handle_close(conn_close) closed! # getting connection.close during connection negotiation means authentication # has failed (RabbitMQ 3.2+): # http://www.rabbitmq.com/auth-notification.html if authenticating? @on_possible_authentication_failure.call(@settings) if @on_possible_authentication_failure end self.exec_callback_yielding_self(:error, conn_close) end # Handles Connection.Close-Ok. # # @api plugin # @see https://www.rabbitmq.com/resources/specs/amqp-xml-doc0-9-1.pdf AMQP 0.9.1 protocol reference (Section 1.4.2.10) def handle_close_ok(close_ok) closed! self.disconnection_successful end # handle_close_ok(close_ok) def handle_connection_blocked(connection_blocked) @on_blocked.call(self, connection_blocked) if @on_blocked end def handle_connection_unblocked(connection_unblocked) @on_unblocked.call(self, connection_unblocked) if @on_unblocked end protected def negotiate_heartbeat_value(client_value, server_value) if client_value == 0 || server_value == 0 [client_value, server_value].max else [client_value, server_value].min end end # Returns next frame from buffer whenever possible # # @api private def get_next_frame return nil unless @chunk_buffer.size > 7 # otherwise, cannot read the length # octet + short offset = 3 # 1 + 2 # length payload_length = @chunk_buffer[offset, 4].unpack(AMQ::Protocol::PACK_UINT32).first # 4 bytes for long payload length, 1 byte final octet frame_length = offset + payload_length + 5 if frame_length <= @chunk_buffer.size @chunk_buffer.slice!(0, frame_length) else nil end end # def get_next_frame # Utility methods # Determines, whether the received frameset is ready to be further processed def frameset_complete?(frames) return false if frames.empty? first_frame = frames[0] first_frame.final? || (first_frame.method_class.has_content? && content_complete?(frames[1..-1])) end # Determines, whether given frame array contains full content body def content_complete?(frames) return false if frames.empty? header = frames[0] raise "Not a content header frame first: #{header.inspect}" unless header.kind_of?(AMQ::Protocol::HeaderFrame) header.body_size == frames[1..-1].inject(0) {|sum, frame| sum + frame.payload.size } end self.handle(AMQ::Protocol::Connection::Start) do |connection, frame| connection.handle_start(frame.decode_payload) end self.handle(AMQ::Protocol::Connection::Tune) do |connection, frame| connection.handle_tune(frame.decode_payload) connection.open(connection.vhost) end self.handle(AMQ::Protocol::Connection::OpenOk) do |connection, frame| connection.handle_open_ok(frame.decode_payload) end self.handle(AMQ::Protocol::Connection::Close) do |connection, frame| connection.handle_close(frame.decode_payload) end self.handle(AMQ::Protocol::Connection::CloseOk) do |connection, frame| connection.handle_close_ok(frame.decode_payload) end self.handle(AMQ::Protocol::Connection::Blocked) do |connection, frame| connection.handle_connection_blocked(frame.decode_payload) end self.handle(AMQ::Protocol::Connection::Unblocked) do |connection, frame| connection.handle_connection_unblocked(frame.decode_payload) end protected def reset @size = 0 @payload = "" @frames = Array.new @chunk_buffer = "" @connection_deferrable = EventMachine::DefaultDeferrable.new @disconnection_deferrable = EventMachine::DefaultDeferrable.new # used to track down whether authentication succeeded. AMQP 0.9.1 dictates # that on authentication failure broker must close TCP connection without sending # any more data. This is why we need to explicitly track whether we are past # authentication stage to signal possible authentication failures. @authenticating = false end def upgrade_to_tls_if_necessary tls_options = @settings[:ssl] if tls_options.is_a?(Hash) start_tls(tls_options) elsif tls_options start_tls end end # upgrade_to_tls_if_necessary private def normalize_auth_mechanism(value) case value when [] then "PLAIN" when nil then "PLAIN" else value end end end # Session end # AMQP amqp-1.8.0/lib/amqp/broker.rb0000644000004100000410000000316713321132064016025 0ustar www-datawww-data# encoding: utf-8 module AMQP # A utility class that makes inspection of broker capabilities easier. class Broker # # API # RABBITMQ_PRODUCT = "RabbitMQ".freeze # Broker information # @return [Hash] # @see Session#server_properties attr_reader :properties # @return [Hash] properties Broker information # @see Session#server_properties def initialize(properties) @properties = properties end # initialize(properties) # @group Product information # @return [Boolean] true if broker is RabbitMQ def rabbitmq? self.product == RABBITMQ_PRODUCT end # rabbitmq? # @return [String] Broker product information def product @product ||= @properties["product"] end # product # @return [String] Broker version def version @version ||= @properties["version"] end # version # @endgroup # @group Product capabilities # @return [Boolean] def supports_publisher_confirmations? @properties["capabilities"]["publisher_confirms"] end # supports_publisher_confirmations? # @return [Boolean] def supports_basic_nack? @properties["capabilities"]["basic.nack"] end # supports_basic_nack? # @return [Boolean] def supports_consumer_cancel_notifications? @properties["capabilities"]["consumer_cancel_notify"] end # supports_consumer_cancel_notifications? # @return [Boolean] def supports_exchange_to_exchange_bindings? @properties["capabilities"]["exchange_exchange_bindings"] end # supports_exchange_to_exchange_bindings? # @endgroup end # Broker end # AMQP amqp-1.8.0/lib/amqp/version.rb0000644000004100000410000000040313321132064016214 0ustar www-datawww-data# encoding: utf-8 module AMQP # amqp gem version. Not to be confused with the AMQP protocol version # it implements. For that, see {AMQ::Protocol::VERSION} # # @see AMQ::Protocol::VERSION # @return [String] AMQP gem version VERSION = '1.8.0' end amqp-1.8.0/lib/amqp/consumer_tag_generator.rb0000644000004100000410000000102713321132064021266 0ustar www-datawww-datamodule AMQP class ConsumerTagGenerator # # API # # @return [String] Generated consumer tag def generate "#{Kernel.rand}-#{Time.now.to_i * 1000}-#{Kernel.rand(999_999_999_999)}" end # generate # @return [String] Generated consumer tag def generate_for(queue) raise ArgumentError, "argument must respond to :name" unless queue.respond_to?(:name) "#{queue.name}-#{Time.now.to_i * 1000}-#{Kernel.rand(999_999_999_999)}" end # generate_for(queue) end # ConsumerTagGenerator end amqp-1.8.0/lib/amqp/settings.rb0000644000004100000410000000122713321132064016374 0ustar www-datawww-data# encoding: utf-8 module AMQP module Settings CLIENT_PROPERTIES = { :platform => ::RUBY_DESCRIPTION, :product => "amqp gem", :information => "http://github.com/ruby-amqp/amqp", :version => AMQP::VERSION, :capabilities => { :publisher_confirms => true, :consumer_cancel_notify => true, :exchange_exchange_bindings => true, :"basic.nack" => true, :"connection.blocked" => true, :authentication_failure_close => true } } def self.client_properties @client_properties ||= CLIENT_PROPERTIES end end end amqp-1.8.0/lib/amqp/exceptions.rb0000644000004100000410000000557113321132064016723 0ustar www-datawww-data# encoding: utf-8 module AMQP # Base class for AMQP connection lifecycle exceptions. # @api public class Error < StandardError # An exception in one of the underlying libraries that caused this # exception to be re-thrown. May be nil. attr_reader :cause end # All the exceptions below are new in 0.8.0. Previous versions # used AMQP::Error for everything. We have to carry this baggage but # there is a way out: simply subclass AMQP::Error and provide a backwards-compatible # way of attaching root cause exception (like AMQ::Client::TCPConnectionFailed) instance # to it. # # In other words: AMQP::Error is here to stay for a long time. MK. # Raised when initial TCP connection to the broker fails. # @api public class TCPConnectionFailed < Error # @return [Hash] connection settings that were used attr_reader :settings def initialize(settings, cause = nil) @settings = settings @cause = cause super("Could not establish TCP connection to #{@settings[:host]}:#{@settings[:port]}") end # TCPConnectionFailed end # Raised when authentication fails. # @api public class PossibleAuthenticationFailureError < Error # @return [Hash] connection settings that were used attr_reader :settings def initialize(settings) @settings = settings super("AMQP broker closed TCP connection before authentication succeeded: this usually means authentication failure due to misconfiguration. Settings are #{filtered_settings.inspect}") end # initialize(settings) def filtered_settings filtered_settings = settings.dup [:pass, :password].each do |sensitve_setting| filtered_settings[sensitve_setting] &&= '[filtered]' end filtered_settings end end # PossibleAuthenticationFailureError # Raised when queue (or exchange) declaration fails because another queue with the same # name but different attributes already exists in the channel object cache. # @api public class IncompatibleOptionsError < Error def initialize(name, opts_1, opts_2) super("There is already an instance called #{name} with options #{opts_1.inspect}, you can't define the same instance with different options (#{opts_2.inspect})!") end end # IncompatibleOptionsError # Raised on attempt to use a channel that was previously closed # (either due to channel-level exception or intentionally via AMQP::Channel#close). # @api public class ChannelClosedError < Error def initialize(instance) super("Channel with id = #{instance.channel} is closed, you can't use it anymore!") end end # ChannelClosedError # Raised on attempt to use a connection that was previously closed # @api public class ConnectionClosedError < Error def initialize(frame) super("The connection is closed, you can't use it anymore!") end end # ConnectionClosedError end # AMQP amqp-1.8.0/lib/amqp/channel.rb0000644000004100000410000017004113321132064016145 0ustar www-datawww-data# encoding: utf-8 require "amqp/int_allocator" require "amqp/exchange" require "amqp/queue" module AMQP # h2. What are AMQP channels # # To quote {https://www.rabbitmq.com/resources/specs/amqp0-9-1.pdf AMQP 0.9.1 specification}: # # AMQP is a multi-channelled protocol. Channels provide a way to multiplex # a heavyweight TCP/IP connection into several light weight connections. # This makes the protocol more “firewall friendly” since port usage is predictable. # It also means that traffic shaping and other network QoS features can be easily employed. # Channels are independent of each other and can perform different functions simultaneously # with other channels, the available bandwidth being shared between the concurrent activities. # # h2. Opening a channel # # *Channels are opened asynchronously*. There are two ways to do it: using a callback or pseudo-synchronous mode. # # @example Opening a channel with a callback # # this assumes EventMachine reactor is running # AMQP.connect("amqp://guest:guest@dev.rabbitmq.com:5672") do |client| # AMQP::Channel.new(client) do |channel, open_ok| # # when this block is executed, channel is open and ready for use # end # end # # # # Unless your application needs multiple channels, this approach is recommended. Alternatively, # AMQP::Channel can be instantiated without a block. Then returned channel is not immediately open, # however, it can be used as if it was a synchronous, blocking method: # # @example Instantiating a channel that will be open eventually # # this assumes EventMachine reactor is running # AMQP.connect("amqp://guest:guest@dev.rabbitmq.com:5672") do |client| # channel = AMQP::Channel.new(client) # exchange = channel.default_exchange # # # ... # end # # # # Even though in the example above channel isn't immediately open, it is safe to declare exchanges using # it. Exchange declaration will be delayed until after channel is open. Same applies to queue declaration # and other operations on exchanges and queues. Library methods that rely on channel being open will be # enqueued and executed in a FIFO manner when broker confirms channel opening. # Note, however, that *this "pseudo-synchronous mode" is easy to abuse and introduce race conditions AMQP gem # cannot resolve for you*. AMQP is an inherently asynchronous protocol and AMQP gem embraces this fact. # # # h2. Key methods # # Key methods of Channel class are # # * {Channel#queue} # * {Channel#default_exchange} # * {Channel#direct} # * {Channel#fanout} # * {Channel#topic} # * {Channel#close} # # refer to documentation for those methods for usage examples. # # Channel provides a number of convenience methods that instantiate queues and exchanges # of various types associated with this channel: # # * {Channel#queue} # * {Channel#default_exchange} # * {Channel#direct} # * {Channel#fanout} # * {Channel#topic} # # # h2. Error handling # # It is possible (and, indeed, recommended) to handle channel-level exceptions by defining an errback using #on_error: # # @example Queue declaration with incompatible attributes results in a channel-level exception # AMQP.start("amqp://guest:guest@dev.rabbitmq.com:5672") do |connection, open_ok| # AMQP::Channel.new do |channel, open_ok| # puts "Channel ##{channel.id} is now open!" # # channel.on_error do |ch, close| # puts "Handling channel-level exception" # # connection.close { # EM.stop { exit } # } # end # # EventMachine.add_timer(0.4) do # # these two definitions result in a race condition. For sake of this example, # # however, it does not matter. Whatever definition succeeds first, 2nd one will # # cause a channel-level exception (because attributes are not identical) # AMQP::Queue.new(channel, "amqpgem.examples.channel_exception", :auto_delete => true, :durable => false) do |queue| # puts "#{queue.name} is ready to go" # end # # AMQP::Queue.new(channel, "amqpgem.examples.channel_exception", :auto_delete => true, :durable => true) do |queue| # puts "#{queue.name} is ready to go" # end # end # end # end # # # # When channel-level exception is indicated by the broker and errback defined using #on_error is run, channel is already # closed and all queue and exchange objects associated with this channel are reset. The recommended way to recover from # channel-level exceptions is to open a new channel and re-instantiate queues, exchanges and bindings your application # needs. # # # # h2. Closing a channel # # Channels are opened when objects is instantiated and closed using {#close} method when application no longer # needs it. # # @example Closing a channel your application no longer needs # # this assumes EventMachine reactor is running # AMQP.connect("amqp://guest:guest@dev.rabbitmq.com:5672") do |client| # AMQP::Channel.new(client) do |channel, open_ok| # channel.close do |close_ok| # # when this block is executed, channel is successfully closed # end # end # end # # # # # h2. RabbitMQ extensions. # # AMQP gem supports several RabbitMQ extensions that extend Channel functionality. # Learn more in {file:docs/VendorSpecificExtensions.textile} # # @see https://www.rabbitmq.com/resources/specs/amqp0-9-1.pdf AMQP 0.9.1 specification (Section 2.2.5) class Channel # # Behaviours # extend RegisterEntityMixin include Entity extend ProtocolMethodHandlers register_entity :queue, AMQP::Queue register_entity :exchange, AMQP::Exchange # # API # # AMQP connection this channel is part of # @return [Connection] attr_reader :connection alias :conn :connection # Status of this channel (one of: :opening, :closing, :open, :closed) # @return [Symbol] attr_reader :status DEFAULT_REPLY_TEXT = "Goodbye".freeze attr_reader :id attr_reader :exchanges_awaiting_declare_ok, :exchanges_awaiting_delete_ok, :exchanges_awaiting_bind_ok, :exchanges_awaiting_unbind_ok attr_reader :queues_awaiting_declare_ok, :queues_awaiting_delete_ok, :queues_awaiting_bind_ok, :queues_awaiting_unbind_ok, :queues_awaiting_purge_ok, :queues_awaiting_get_response attr_reader :consumers_awaiting_consume_ok, :consumers_awaiting_cancel_ok attr_accessor :flow_is_active # Change publisher index. Publisher index is incremented # by 1 after each Basic.Publish starting at 1. This is done # on both client and server, hence this acknowledged messages # can be matched via its delivery-tag. # # @api private attr_writer :publisher_index # @param [AMQP::Session] connection Connection to open this channel on. If not given, default AMQP # connection (accessible via {AMQP.connection}) will be used. # @param [Integer] id Channel id. Must not be greater than max channel id client and broker # negotiated on during connection setup. Almost always the right thing to do # is to let AMQP gem pick channel identifier for you. # @param [Hash] options A hash of options # # @example Instantiating a channel for default connection (accessible as AMQP.connection) # # AMQP.connect do |connection| # AMQP::Channel.new(connection) do |channel, open_ok| # # channel is ready: set up your messaging flow by creating exchanges, # # queues, binding them together and so on. # end # end # # @example Instantiating a channel for explicitly given connection # # AMQP.connect do |connection| # AMQP::Channel.new(connection) do |channel, open_ok| # # ... # end # end # # @example Instantiating a channel with a :prefetch option # # AMQP.connect do |connection| # AMQP::Channel.new(connection, :prefetch => 5) do |channel, open_ok| # # ... # end # end # # # @option options [Boolean] :prefetch (nil) Specifies number of messages to prefetch. Channel-specific. See {AMQP::Channel#prefetch}. # @option options [Boolean] :auto_recovery (nil) Turns on automatic network failure recovery mode for this channel. # # @yield [channel, open_ok] Yields open channel instance and AMQP method (channel.open-ok) instance. The latter is optional. # @yieldparam [Channel] channel Channel that is successfully open # @yieldparam [AMQP::Protocol::Channel::OpenOk] open_ok AMQP channel.open-ok) instance # # # @see AMQP::Channel#prefetch # @api public def initialize(connection = nil, id = nil, options = {}, &block) raise 'AMQP can only be used from within EM.run {}' unless EM.reactor_running? @connection = connection || AMQP.connection || AMQP.start # this means 2nd argument is options if id.kind_of?(Hash) options = options.merge(id) id = @connection.next_channel_id end super(@connection) @id = id || @connection.next_channel_id @exchanges = Hash.new @queues = Hash.new @consumers = Hash.new @options = { :auto_recovery => @connection.auto_recovering? }.merge(options) @auto_recovery = (!!@options[:auto_recovery]) # we must synchronize frameset delivery. MK. @mutex = Mutex.new reset_state! # 65536 is here for cases when channel is opened without passing a callback in, # otherwise channel_mix would be nil and it causes a lot of needless headaches. # lets just have this default. MK. channel_max = if @connection.open? @connection.channel_max || 65536 else 65536 end if channel_max != 0 && !(0..channel_max).include?(@id) raise ArgumentError.new("Max channel for the connection is #{channel_max}, given: #{@id}") end # we need this deferrable to mimic what AMQP gem 0.7 does to enable # the following (pseudo-synchronous) style of programming some people use in their # existing codebases: # # connection = AMQP.connect # channel = AMQP::Channel.new(connection) # queue = AMQP::Queue.new(channel) # # ... # # Read more about EM::Deferrable#callback behavior in EventMachine documentation. MK. @channel_is_open_deferrable = AMQP::Deferrable.new @parameter_checks = {:queue => [:durable, :exclusive, :auto_delete, :arguments], :exchange => [:type, :durable, :arguments]} # only send channel.open when connection is actually open. Makes it possible to # do c = AMQP.connect; AMQP::Channel.new(c) that is what some people do. MK. @connection.on_connection do self.open do |ch, open_ok| @channel_is_open_deferrable.succeed if block case block.arity when 1 then block.call(ch) else block.call(ch, open_ok) end # case end # if self.prefetch(@options[:prefetch], false) if @options[:prefetch] end # self.open end # @connection.on_open end # @return [Boolean] true if this channel is in automatic recovery mode # @see #auto_recovering? attr_accessor :auto_recovery # @return [Boolean] true if this channel uses automatic recovery mode def auto_recovering? @auto_recovery end # auto_recovering? # Called by associated connection object when AMQP connection has been re-established # (for example, after a network failure). # # @api plugin def auto_recover return unless auto_recovering? @channel_is_open_deferrable.fail @channel_is_open_deferrable = AMQP::Deferrable.new self.open do @channel_is_open_deferrable.succeed # re-establish prefetch self.prefetch(@options[:prefetch], false) if @options[:prefetch] # exchanges must be recovered first because queue recovery includes recovery of bindings. MK. @exchanges.each { |name, e| e.auto_recover } @queues.each { |name, q| q.auto_recover } end end # auto_recover # Can be used to recover channels from channel-level exceptions. Allocates a new channel id and reopens # itself with this new id, releasing the old id after the new one is allocated. # # This includes recovery of known exchanges, queues and bindings, exactly the same way as when # the client recovers from a network failure. # # @api public def reuse old_id = @id # must release after we allocate a new id, otherwise we will end up # with the same value. MK. @id = @connection.next_channel_id @connection.release_channel_id(old_id) @channel_is_open_deferrable.fail @channel_is_open_deferrable = AMQP::Deferrable.new self.open do @channel_is_open_deferrable.succeed # re-establish prefetch self.prefetch(@options[:prefetch], false) if @options[:prefetch] # exchanges must be recovered first because queue recovery includes recovery of bindings. MK. @exchanges.each { |name, e| e.auto_recover } @queues.each { |name, q| q.auto_recover } end end # reuse # @group Declaring exchanges # Defines, intializes and returns a direct Exchange instance. # # Learn more about direct exchanges in {Exchange Exchange class documentation}. # # # @param [String] name (amq.direct) Exchange name. # # @option opts [Boolean] :passive (false) If set, the server will not create the exchange if it does not # already exist. The client can use this to check whether an exchange # exists without modifying the server state. # # @option opts [Boolean] :durable (false) If set when creating a new exchange, the exchange will be marked as # durable. Durable exchanges and their bindings are recreated upon a server # restart (information about them is persisted). Non-durable (transient) exchanges # do not survive if/when a server restarts (information about them is stored exclusively # in RAM). # # # @option opts [Boolean] :auto_delete (false) If set, the exchange is deleted when all queues have finished # using it. The server waits for a short period of time before # determining the exchange is unused to give time to the client code # to bind a queue to it. # # @option opts [Boolean] :internal (default false) 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. This is a RabbitMQ-specific # extension. # # @option opts [Boolean] :nowait (true) 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. # # # @example Using default pre-declared direct exchange and no callbacks (pseudo-synchronous style) # # # an exchange application A will be using to publish updates # # to some search index # exchange = channel.direct("index.updates") # # # In the same (or different) process declare a queue that broker will # # generate name for, bind it to aforementioned exchange using method chaining # queue = channel.queue(""). # # queue will be receiving messages that were published with # # :routing_key attribute value of "search.index.updates" # bind(exchange, :routing_key => "search.index.updates"). # # register a callback that will be run when messages arrive # subscribe { |header, message| puts("Received #{message}") } # # # now publish a new document contents for indexing, # # message will be delivered to the queue we declared and bound on the line above # exchange.publish(document.content, :routing_key => "search.index.updates") # # # @example Instantiating a direct exchange using {Channel#direct} with a callback # # AMQP.connect do |connection| # AMQP::Channel.new(connection) do |channel| # channel.direct("email.replies_listener") do |exchange, declare_ok| # # by now exchange is ready and waiting # end # end # end # # # @see Channel#default_exchange # @see Exchange # @see Exchange#initialize # @see https://www.rabbitmq.com/resources/specs/amqp0-9-1.pdf AMQP 0.9.1 specification (Section 3.1.3.1) # # @return [Exchange] # @api public def direct(name = 'amq.direct', opts = {}, &block) if exchange = find_exchange(name) extended_opts = Exchange.add_default_options(:direct, name, opts, block) validate_parameters_match!(exchange, extended_opts, :exchange) block.call(exchange) if block exchange else register_exchange(Exchange.new(self, :direct, name, opts, &block)) end end # Returns exchange object with the same name as default (aka unnamed) exchange. # Default exchange is a direct exchange and automatically routes messages to # queues when routing key matches queue name exactly. This feature is known as # "automatic binding" (of queues to default exchange). # # *Use default exchange when you want to route messages directly to specific queues* # (queue names are known, you don't mind this kind of coupling between applications). # # # @example Using default exchange to publish messages to queues with known names # AMQP.start(:host => 'localhost') do |connection| # ch = AMQP::Channel.new(connection) # # queue1 = ch.queue("queue1").subscribe do |payload| # puts "[#{queue1.name}] => #{payload}" # end # queue2 = ch.queue("queue2").subscribe do |payload| # puts "[#{queue2.name}] => #{payload}" # end # queue3 = ch.queue("queue3").subscribe do |payload| # puts "[#{queue3.name}] => #{payload}" # end # queues = [queue1, queue2, queue3] # # # Rely on default direct exchange binding, see section 2.1.2.4 Automatic Mode in AMQP 0.9.1 spec. # exchange = AMQP::Exchange.default # EM.add_periodic_timer(1) do # q = queues.sample # # exchange.publish "Some payload from #{Time.now.to_i}", :routing_key => q.name # end # end # # # # @see Exchange # @see https://www.rabbitmq.com/resources/specs/amqp0-9-1.pdf AMQP 0.9.1 specification (Section 2.1.2.4) # # @return [Exchange] # @api public def default_exchange @default_exchange ||= Exchange.default(self) end # Defines, intializes and returns a fanout Exchange instance. # # Learn more about fanout exchanges in {Exchange Exchange class documentation}. # # # @param [String] name (amq.fanout) Exchange name. # # @option opts [Boolean] :passive (false) If set, the server will not create the exchange if it does not # already exist. The client can use this to check whether an exchange # exists without modifying the server state. # # @option opts [Boolean] :durable (false) If set when creating a new exchange, the exchange will be marked as # durable. Durable exchanges and their bindings are recreated upon a server # restart (information about them is persisted). Non-durable (transient) exchanges # do not survive if/when a server restarts (information about them is stored exclusively # in RAM). # # # @option opts [Boolean] :auto_delete (false) If set, the exchange is deleted when all queues have finished # using it. The server waits for a short period of time before # determining the exchange is unused to give time to the client code # to bind a queue to it. # # @option opts [Boolean] :internal (default false) 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. This is a RabbitMQ-specific # extension. # # @option opts [Boolean] :nowait (true) 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. # # @example Using fanout exchange to deliver messages to multiple consumers # # # open up a channel # # declare a fanout exchange # # declare 3 queues, binds them # # publish a message # # @see Exchange # @see Exchange#initialize # @see Channel#default_exchange # @see https://www.rabbitmq.com/resources/specs/amqp0-9-1.pdf AMQP 0.9.1 specification (Section 3.1.3.2) # # @return [Exchange] # @api public def fanout(name = 'amq.fanout', opts = {}, &block) if exchange = find_exchange(name) extended_opts = Exchange.add_default_options(:fanout, name, opts, block) validate_parameters_match!(exchange, extended_opts, :exchange) block.call(exchange) if block exchange else register_exchange(Exchange.new(self, :fanout, name, opts, &block)) end end # Defines, intializes and returns a topic Exchange instance. # # Learn more about topic exchanges in {Exchange Exchange class documentation}. # # @param [String] name (amq.topic) Exchange name. # # # @option opts [Boolean] :passive (false) If set, the server will not create the exchange if it does not # already exist. The client can use this to check whether an exchange # exists without modifying the server state. # # @option opts [Boolean] :durable (false) If set when creating a new exchange, the exchange will be marked as # durable. Durable exchanges and their bindings are recreated upon a server # restart (information about them is persisted). Non-durable (transient) exchanges # do not survive if/when a server restarts (information about them is stored exclusively # in RAM). # # # @option opts [Boolean] :auto_delete (false) If set, the exchange is deleted when all queues have finished # using it. The server waits for a short period of time before # determining the exchange is unused to give time to the client code # to bind a queue to it. # # @option opts [Boolean] :internal (default false) 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. This is a RabbitMQ-specific # extension. # # @option opts [Boolean] :nowait (true) 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. # # @example Using topic exchange to deliver relevant news updates # AMQP.connect do |connection| # channel = AMQP::Channel.new(connection) # exchange = channel.topic("pub/sub") # # # Subscribers. # channel.queue("development").bind(exchange, :key => "technology.dev.#").subscribe do |payload| # puts "A new dev post: '#{payload}'" # end # channel.queue("ruby").bind(exchange, :key => "technology.#.ruby").subscribe do |payload| # puts "A new post about Ruby: '#{payload}'" # end # # # Let's publish some data. # exchange.publish "Ruby post", :routing_key => "technology.dev.ruby" # exchange.publish "Erlang post", :routing_key => "technology.dev.erlang" # exchange.publish "Sinatra post", :routing_key => "technology.web.ruby" # exchange.publish "Jewelery post", :routing_key => "jewelery.ruby" # end # # # @example Using topic exchange to deliver geographically-relevant data # AMQP.connect do |connection| # channel = AMQP::Channel.new(connection) # exchange = channel.topic("pub/sub") # # # Subscribers. # channel.queue("americas.north").bind(exchange, :routing_key => "americas.north.#").subscribe do |headers, payload| # puts "An update for North America: #{payload}, routing key is #{headers.routing_key}" # end # channel.queue("americas.south").bind(exchange, :routing_key => "americas.south.#").subscribe do |headers, payload| # puts "An update for South America: #{payload}, routing key is #{headers.routing_key}" # end # channel.queue("us.california").bind(exchange, :routing_key => "americas.north.us.ca.*").subscribe do |headers, payload| # puts "An update for US/California: #{payload}, routing key is #{headers.routing_key}" # end # channel.queue("us.tx.austin").bind(exchange, :routing_key => "#.tx.austin").subscribe do |headers, payload| # puts "An update for Austin, TX: #{payload}, routing key is #{headers.routing_key}" # end # channel.queue("it.rome").bind(exchange, :routing_key => "europe.italy.rome").subscribe do |headers, payload| # puts "An update for Rome, Italy: #{payload}, routing key is #{headers.routing_key}" # end # channel.queue("asia.hk").bind(exchange, :routing_key => "asia.southeast.hk.#").subscribe do |headers, payload| # puts "An update for Hong Kong: #{payload}, routing key is #{headers.routing_key}" # end # # exchange.publish("San Diego update", :routing_key => "americas.north.us.ca.sandiego"). # publish("Berkeley update", :routing_key => "americas.north.us.ca.berkeley"). # publish("San Francisco update", :routing_key => "americas.north.us.ca.sanfrancisco"). # publish("New York update", :routing_key => "americas.north.us.ny.newyork"). # publish("São Paolo update", :routing_key => "americas.south.brazil.saopaolo"). # publish("Hong Kong update", :routing_key => "asia.southeast.hk.hongkong"). # publish("Kyoto update", :routing_key => "asia.southeast.japan.kyoto"). # publish("Shanghai update", :routing_key => "asia.southeast.prc.shanghai"). # publish("Rome update", :routing_key => "europe.italy.roma"). # publish("Paris update", :routing_key => "europe.france.paris") # end # # @see Exchange # @see Exchange#initialize # @see http://www.rabbitmq.com/faq.html#Binding-and-Routing RabbitMQ FAQ on routing & wildcards # @see https://www.rabbitmq.com/resources/specs/amqp0-9-1.pdf AMQP 0.9.1 specification (Section 3.1.3.3) # # @return [Exchange] # @api public def topic(name = 'amq.topic', opts = {}, &block) if exchange = find_exchange(name) extended_opts = Exchange.add_default_options(:topic, name, opts, block) validate_parameters_match!(exchange, extended_opts, :exchange) block.call(exchange) if block exchange else register_exchange(Exchange.new(self, :topic, name, opts, &block)) end end # Defines, intializes and returns a headers Exchange instance. # # Learn more about headers exchanges in {Exchange Exchange class documentation}. # # @param [String] name (amq.match) Exchange name. # # @option opts [Boolean] :passive (false) If set, the server will not create the exchange if it does not # already exist. The client can use this to check whether an exchange # exists without modifying the server state. # # @option opts [Boolean] :durable (false) If set when creating a new exchange, the exchange will be marked as # durable. Durable exchanges and their bindings are recreated upon a server # restart (information about them is persisted). Non-durable (transient) exchanges # do not survive if/when a server restarts (information about them is stored exclusively # in RAM). # # # @option opts [Boolean] :auto_delete (false) If set, the exchange is deleted when all queues have finished # using it. The server waits for a short period of time before # determining the exchange is unused to give time to the client code # to bind a queue to it. # # @option opts [Boolean] :internal (default false) 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. This is a RabbitMQ-specific # extension. # # @option opts [Boolean] :nowait (true) 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. # # # @example Using headers exchange to route messages based on multiple attributes (OS, architecture, # of cores) # # puts "=> Headers routing example" # puts # AMQP.start do |connection| # channel = AMQP::Channel.new(connection) # channel.on_error do |ch, channel_close| # puts "A channel-level exception: #{channel_close.inspect}" # end # # exchange = channel.headers("amq.match", :durable => true) # # channel.queue("", :auto_delete => true).bind(exchange, :arguments => { 'x-match' => 'all', :arch => "x64", :os => 'linux' }).subscribe do |metadata, payload| # puts "[linux/x64] Got a message: #{payload}" # end # channel.queue("", :auto_delete => true).bind(exchange, :arguments => { 'x-match' => 'all', :arch => "x32", :os => 'linux' }).subscribe do |metadata, payload| # puts "[linux/x32] Got a message: #{payload}" # end # channel.queue("", :auto_delete => true).bind(exchange, :arguments => { 'x-match' => 'any', :os => 'linux', :arch => "__any__" }).subscribe do |metadata, payload| # puts "[linux] Got a message: #{payload}" # end # channel.queue("", :auto_delete => true).bind(exchange, :arguments => { 'x-match' => 'any', :os => 'macosx', :cores => 8 }).subscribe do |metadata, payload| # puts "[macosx|octocore] Got a message: #{payload}" # end # # # EventMachine.add_timer(0.5) do # exchange.publish "For linux/x64", :headers => { :arch => "x64", :os => 'linux' } # exchange.publish "For linux/x32", :headers => { :arch => "x32", :os => 'linux' } # exchange.publish "For linux", :headers => { :os => 'linux' } # exchange.publish "For OS X", :headers => { :os => 'macosx' } # exchange.publish "For solaris/x64", :headers => { :os => 'solaris', :arch => 'x64' } # exchange.publish "For ocotocore", :headers => { :cores => 8 } # end # # # show_stopper = Proc.new do # $stdout.puts "Stopping..." # connection.close { # EventMachine.stop { exit } # } # end # # Signal.trap "INT", show_stopper # EventMachine.add_timer(2, show_stopper) # end # # # # @see Exchange # @see Exchange#initialize # @see Channel#default_exchange # @see https://www.rabbitmq.com/resources/specs/amqp0-9-1.pdf AMQP 0.9.1 specification (Section 3.1.3.3) # # @return [Exchange] # @api public def headers(name = 'amq.match', opts = {}, &block) if exchange = find_exchange(name) extended_opts = Exchange.add_default_options(:headers, name, opts, block) validate_parameters_match!(exchange, extended_opts, :exchange) block.call(exchange) if block exchange else register_exchange(Exchange.new(self, :headers, name, opts, &block)) end end # @endgroup # @group Declaring queues # Declares and returns a Queue instance associated with this channel. See {Queue Queue class documentation} for # more information about queues. # # To make broker generate queue name for you (a classic example is exclusive # queues that are only used for a short period of time), pass empty string # as name value. Then queue will get it's name as soon as broker's response # (queue.declare-ok) arrives. Note that in this case, block is required. # # # Like for exchanges, queue names starting with 'amq.' cannot be modified and # should not be used by applications. # # @example Declaring a queue in a mail delivery app using Channel#queue without a block # AMQP.connect do |connection| # AMQP::Channel.new(connection) do |ch| # # message producers will be able to send messages to this queue # # using direct exchange and routing key = "mail.delivery" # queue = ch.queue("mail.delivery", :durable => true) # queue.subscribe do |headers, payload| # # ... # end # end # end # # @example Declaring a server-named exclusive queue that receives all messages related to events, using a block. # AMQP.connect do |connection| # AMQP::Channel.new(connection) do |ch| # # message producers will be able to send messages to this queue # # using amq.topic exchange with routing keys that begin with "events" # ch.queue("", :exclusive => true) do |queue| # queue.bind(ch.exchange("amq.topic"), :routing_key => "events.#").subscribe do |headers, payload| # # ... # end # end # end # end # # @param [String] name Queue name. If you want a server-named queue, you can omit the name (note that in this case, using block is mandatory). # See {Queue Queue class documentation} for discussion of queue lifecycles and when use of server-named queues # is optimal. # # @option opts [Boolean] :passive (false) If set, the server will not create the exchange if it does not # already exist. The client can use this to check whether an exchange # exists without modifying the server state. # # @option opts [Boolean] :durable (false) If set when creating a new exchange, the exchange will be marked as # durable. Durable exchanges and their bindings are recreated upon a server # restart (information about them is persisted). Non-durable (transient) exchanges # do not survive if/when a server restarts (information about them is stored exclusively # in RAM). Any remaining messages in the queue will be purged when the queue # is deleted regardless of the message's persistence setting. # # # @option opts [Boolean] :auto_delete (false) If set, the exchange is deleted when all queues have finished # using it. The server waits for a short period of time before # determining the exchange is unused to give time to the client code # to bind a queue to it. # # @option opts [Boolean] :exclusive (false) Exclusive queues may only be used by a single connection. # Exclusivity also implies that queue is automatically deleted when connection # is closed. Only one consumer is allowed to remove messages from exclusive queue. # # @option opts [Boolean] :nowait (true) 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. # # @yield [queue, declare_ok] Yields successfully declared queue instance and AMQP method (queue.declare-ok) instance. The latter is optional. # @yieldparam [Queue] queue Queue that is successfully declared and is ready to be used. # @yieldparam [AMQP::Protocol::Queue::DeclareOk] declare_ok AMQP queue.declare-ok) instance. # # @see Queue # @see Queue#initialize # @see https://www.rabbitmq.com/resources/specs/amqp0-9-1.pdf AMQP 0.9.1 specification (Section 2.1.4) # # @return [Queue] # @api public def queue(name = AMQ::Protocol::EMPTY_STRING, opts = {}, &block) raise ArgumentError.new("queue name must not be nil; if you want broker to generate queue name for you, pass an empty string") if name.nil? if name && !name.empty? && (queue = find_queue(name)) extended_opts = Queue.add_default_options(name, opts, block) validate_parameters_match!(queue, extended_opts, :queue) block.call(queue) if block queue else self.queue!(name, opts, &block) end end # Same as {Channel#queue} but when queue with the same name already exists in this channel # object's cache, this method will replace existing queue with a newly defined one. Consider # using {Channel#queue} instead. # # @see Channel#queue # # @return [Queue] # @api public def queue!(name, opts = {}, &block) queue = if block.nil? Queue.new(self, name, opts) else shim = Proc.new { |q, method| if block.arity == 1 block.call(q) else queue = find_queue(method.queue) block.call(queue, method.consumer_count, method.message_count) end } Queue.new(self, name, opts, &shim) end register_queue(queue) end # @return [Array] Queues cache for this channel # @api plugin # @private def queues @queues end # queues # @endgroup # @group Channel lifecycle # Opens AMQP channel. # # @note Instantiated channels are opened by default. This method should only be used for error recovery after network connection loss. # @api public def open(&block) @connection.send_frame(AMQ::Protocol::Channel::Open.encode(@id, AMQ::Protocol::EMPTY_STRING)) @connection.channels[@id] = self self.status = :opening self.redefine_callback :open, &block end alias reopen open # @return [Boolean] true if channel is not closed. # @api public def open? self.status == :opened || self.status == :opening end # open? # Takes a block that will be deferred till the moment when channel is considered open # (channel.open-ok is received from the broker). If you need to delay an operation # till the moment channel is open, this method is what you are looking for. # # Multiple callbacks are supported. If when this moment is called, channel is already # open, block is executed immediately. # # @api public def once_open(&block) @channel_is_open_deferrable.callback do # guards against cases when deferred operations # don't complete before the channel is closed block.call if open? end end # once_open(&block) alias once_opened once_open # @return [Boolean] # @api public def closing? self.status == :closing end # Closes AMQP channel. # # @api public def close(reply_code = 200, reply_text = DEFAULT_REPLY_TEXT, class_id = 0, method_id = 0, &block) self.once_open do self.status = :closing @connection.send_frame(AMQ::Protocol::Channel::Close.encode(@id, reply_code, reply_text, class_id, method_id)) self.redefine_callback :close, &block end end # @endgroup # @group QoS and flow handling # Asks the peer to pause or restart the flow of content data sent to 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 to # Queue#get callers. # # @param [Boolean] Desired flow state. # # @see https://www.rabbitmq.com/resources/specs/amqp-xml-doc0-9-1.pdf AMQP 0.9.1 protocol documentation (Section 1.5.2.3.) # @api public def flow(active = false, &block) @connection.send_frame(AMQ::Protocol::Channel::Flow.encode(@id, active)) self.redefine_callback :flow, &block self end # @return [Boolean] True if flow in this channel is active (messages will be delivered to consumers that use this channel). # # @api public def flow_is_active? @flow_is_active end # flow_is_active? # @param [Fixnum] Message count # @param [Boolean] global (false) # # @return [Channel] self # # @api public def prefetch(count, global = false, &block) self.once_open do # RabbitMQ does not support prefetch_size. self.qos(0, count, global, &block) @options[:prefetch] = count end self end # @endgroup # @group Message acknowledgements # Acknowledge one or all messages on the channel. # # @api public # @see #reject # @see #recover # @see https://www.rabbitmq.com/resources/specs/amqp-xml-doc0-9-1.pdf AMQP 0.9.1 protocol documentation (Section 1.8.3.13.) def acknowledge(delivery_tag, multiple = false) @connection.send_frame(AMQ::Protocol::Basic::Ack.encode(self.id, delivery_tag, multiple)) self end # acknowledge(delivery_tag, multiple = false) # Reject a message with given delivery tag. # # @api public # @see #acknowledge # @see #recover # @see https://www.rabbitmq.com/resources/specs/amqp-xml-doc0-9-1.pdf AMQP 0.9.1 protocol documentation (Section 1.8.3.14.) def reject(delivery_tag, requeue = true, multi = false) if multi @connection.send_frame(AMQ::Protocol::Basic::Nack.encode(self.id, delivery_tag, multi, requeue)) else @connection.send_frame(AMQ::Protocol::Basic::Reject.encode(self.id, delivery_tag, requeue)) end self end # reject(delivery_tag, requeue = true) # Notifies AMQ broker that consumer has recovered and unacknowledged messages need # to be redelivered. # # @return [Channel] self # # @note RabbitMQ as of 2.3.1 does not support basic.recover with requeue = false. # @see https://www.rabbitmq.com/resources/specs/amqp-xml-doc0-9-1.pdf AMQP 0.9.1 protocol documentation (Section 1.8.3.16.) # @see #acknowledge # @api public def recover(requeue = true, &block) @connection.send_frame(AMQ::Protocol::Basic::Recover.encode(@id, requeue)) self.redefine_callback :recover, &block self end # recover(requeue = false, &block) # @endgroup # @group Transactions # Sets the channel to use standard transactions. One must use this method at least # once on a channel before using #tx_tommit or tx_rollback methods. # # @api public def tx_select(&block) @connection.send_frame(AMQ::Protocol::Tx::Select.encode(@id)) self.redefine_callback :tx_select, &block self end # tx_select(&block) # Commits AMQP transaction. # # @api public def tx_commit(&block) @connection.send_frame(AMQ::Protocol::Tx::Commit.encode(@id)) self.redefine_callback :tx_commit, &block self end # tx_commit(&block) # Rolls AMQP transaction back. # # @api public def tx_rollback(&block) @connection.send_frame(AMQ::Protocol::Tx::Rollback.encode(@id)) self.redefine_callback :tx_rollback, &block self end # tx_rollback(&block) # @endgroup # @group Error handling # Defines a callback that will be executed when channel is closed after # channel-level exception. # # @api public def on_error(&block) self.define_callback(:error, &block) end # @endgroup # @group Publisher Confirms def confirm_select(nowait = false, &block) self.once_open do if nowait && block raise ArgumentError, "confirm.select with nowait = true and a callback makes no sense" end @uses_publisher_confirmations = true reset_publisher_index! self.redefine_callback(:confirm_select, &block) unless nowait self.redefine_callback(:after_publish) do increment_publisher_index! end @connection.send_frame(AMQ::Protocol::Confirm::Select.encode(@id, nowait)) self end end # @endgroup # # Implementation # # Resets channel state (for example, list of registered queue objects and so on). # # Most of the time, this method is not # called by application code. # # @private # @api plugin def reset(&block) # See AMQP::Channel self.reset_state! # there is no way to reset a deferrable; we have to use a new instance. MK. @channel_is_open_deferrable = AMQP::Deferrable.new @channel_is_open_deferrable.callback(&block) @connection.on_connection do @channel_is_open_deferrable.succeed self.prefetch(@options[:prefetch], false) if @options[:prefetch] end end # Overrides superclass method to also re-create @channel_is_open_deferrable # # @api plugin # @private def handle_connection_interruption(method = nil) @queues.each { |name, q| q.handle_connection_interruption(method) } @exchanges.each { |name, e| e.handle_connection_interruption(method) } self.exec_callback_yielding_self(:after_connection_interruption) self.reset_state! @connection.release_channel_id(@id) unless auto_recovering? @channel_is_open_deferrable = AMQP::Deferrable.new end # @return [Hash] def consumers @consumers end # consumers # @return [Hash] Collection of exchanges that were declared on this channel. def exchanges @exchanges end # AMQP connection this channel belongs to. # # @return [AMQP::Connection] Connection this channel belongs to. def connection @connection end # connection # Synchronizes given block using this channel's mutex. # @api public def synchronize(&block) @mutex.synchronize(&block) end # @group QoS and flow handling # Requests a specific quality of service. The QoS can be specified for the current channel # or for all channels on the connection. # # @note RabbitMQ as of 2.3.1 does not support prefetch_size. # @api public def qos(prefetch_size = 0, prefetch_count = 32, global = false, &block) @connection.send_frame(AMQ::Protocol::Basic::Qos.encode(@id, prefetch_size, prefetch_count, global)) self.redefine_callback :qos, &block self end # qos # @endgroup # @group Error handling # Defines a callback that will be executed after TCP connection is interrupted (typically because of a network failure). # Only one callback can be defined (the one defined last replaces previously added ones). # # @api public def on_connection_interruption(&block) self.redefine_callback(:after_connection_interruption, &block) end # on_connection_interruption(&block) alias after_connection_interruption on_connection_interruption # Defines a callback that will be executed after TCP connection has recovered after a network failure # but before AMQP connection is re-opened. # Only one callback can be defined (the one defined last replaces previously added ones). # # @api public def before_recovery(&block) self.redefine_callback(:before_recovery, &block) end # before_recovery(&block) # @private def run_before_recovery_callbacks self.exec_callback_yielding_self(:before_recovery) @queues.each { |name, q| q.run_before_recovery_callbacks } @exchanges.each { |name, e| e.run_before_recovery_callbacks } end # Defines a callback that will be executed after AMQP connection has recovered after a network failure. # Only one callback can be defined (the one defined last replaces previously added ones). # # @api public def on_recovery(&block) self.redefine_callback(:after_recovery, &block) end # on_recovery(&block) alias after_recovery on_recovery # @private def run_after_recovery_callbacks self.exec_callback_yielding_self(:after_recovery) @queues.each { |name, q| q.run_after_recovery_callbacks } @exchanges.each { |name, e| e.run_after_recovery_callbacks } end # @endgroup # Publisher index is an index of the last message since # the confirmations were activated, started with 0. It's # incremented by 1 every time a message is published. # This is done on both client and server, hence this # acknowledged messages can be matched via its delivery-tag. # # @return [Integer] Current publisher index. # @api public def publisher_index @publisher_index ||= 0 end # Resets publisher index to 0 # # @api plugin def reset_publisher_index! @publisher_index = 0 end # This method is executed after publishing of each message via {Exchage#publish}. # Currently it just increments publisher index by 1, so messages # can be actually matched. # # @api plugin def increment_publisher_index! @publisher_index += 1 end # @return [Boolean] def uses_publisher_confirmations? @uses_publisher_confirmations end # uses_publisher_confirmations? # Turn on confirmations for this channel and, if given, # register callback for basic.ack from the broker. # # @raise [RuntimeError] Occurs when confirmations are already activated. # @raise [RuntimeError] Occurs when nowait is true and block is given. # @param [Boolean] nowait Whether we expect Confirm.Select-Ok to be returned by the broker or not. # # @yield [basick_ack] Callback which will be executed every time we receive Basic.Ack from the broker. # @yieldparam [AMQ::Protocol::Basic::Ack] basick_ack Protocol method class instance. # # @return [self] self. def on_ack(nowait = false, &block) self.define_callback(:ack, &block) if block self end # Register error callback for Basic.Nack. It's called # when message(s) is rejected. # # @return [self] self def on_nack(&block) self.define_callback(:nack, &block) if block self end # Handler for Confirm.Select-Ok. By default, it just # executes hook specified via the #confirmations method # with a single argument, a protocol method class # instance (an instance of AMQ::Protocol::Confirm::SelectOk) # and then it deletes the callback, since Confirm.Select # is supposed to be sent just once. # # @api plugin def handle_select_ok(method) self.exec_callback_once(:confirm_select, method) end # Handler for Basic.Ack. By default, it just # executes hook specified via the #confirm method # with a single argument, a protocol method class # instance (an instance of AMQ::Protocol::Basic::Ack). # # @api plugin def handle_basic_ack(method) self.exec_callback(:ack, method) end # Handler for Basic.Nack. By default, it just # executes hook specified via the #confirm_failed method # with a single argument, a protocol method class # instance (an instance of AMQ::Protocol::Basic::Nack). # # @api plugin def handle_basic_nack(method) self.exec_callback(:nack, method) end # # Implementation # def register_exchange(exchange) raise ArgumentError, "argument is nil!" if exchange.nil? @exchanges[exchange.name] = exchange end # register_exchange(exchange) # Finds exchange in the exchanges cache on this channel by name. Exchange only exists in the cache if # it was previously instantiated on this channel. # # @param [String] name Exchange name # @return [AMQP::Exchange] Exchange (if found) # @api plugin def find_exchange(name) @exchanges[name] end # @api plugin # @private def register_queue(queue) raise ArgumentError, "argument is nil!" if queue.nil? @queues[queue.name] = queue end # register_queue(queue) # @api plugin # @private def find_queue(name) @queues[name] end RECOVERY_EVENTS = [:after_connection_interruption, :before_recovery, :after_recovery, :error].freeze # @api plugin # @private def reset_state! @flow_is_active = true @queues_awaiting_declare_ok = Array.new @exchanges_awaiting_declare_ok = Array.new @exchanges_awaiting_bind_ok = Array.new @exchanges_awaiting_unbind_ok = Array.new @queues_awaiting_delete_ok = Array.new @exchanges_awaiting_delete_ok = Array.new @queues_awaiting_purge_ok = Array.new @queues_awaiting_bind_ok = Array.new @queues_awaiting_unbind_ok = Array.new @consumers_awaiting_consume_ok = Array.new @consumers_awaiting_cancel_ok = Array.new @queues_awaiting_get_response = Array.new @callbacks = @callbacks.delete_if { |k, v| !RECOVERY_EVENTS.include?(k) } @uses_publisher_confirmations = false end # reset_state! # @api plugin # @private def handle_open_ok(open_ok) self.status = :opened self.exec_callback_once_yielding_self(:open, open_ok) end # @api plugin # @private def handle_close_ok(close_ok) self.status = :closed self.connection.clear_frames_on(self.id) self.exec_callback_once_yielding_self(:close, close_ok) @connection.release_channel_id(@id) end # @api plugin # @private def handle_close(channel_close) self.status = :closed self.connection.clear_frames_on(self.id) self.exec_callback_yielding_self(:error, channel_close) end self.handle(AMQ::Protocol::Channel::OpenOk) do |connection, frame| channel = connection.channels[frame.channel] channel.handle_open_ok(frame.decode_payload) end self.handle(AMQ::Protocol::Channel::CloseOk) do |connection, frame| method = frame.decode_payload channels = connection.channels channel = channels[frame.channel] channels.delete(channel) channel.handle_close_ok(method) end self.handle(AMQ::Protocol::Channel::Close) do |connection, frame| method = frame.decode_payload channels = connection.channels channel = channels[frame.channel] connection.send_frame(AMQ::Protocol::Channel::CloseOk.encode(frame.channel)) channel.handle_close(method) end self.handle(AMQ::Protocol::Basic::QosOk) do |connection, frame| channel = connection.channels[frame.channel] channel.exec_callback(:qos, frame.decode_payload) end self.handle(AMQ::Protocol::Basic::RecoverOk) do |connection, frame| channel = connection.channels[frame.channel] channel.exec_callback(:recover, frame.decode_payload) end self.handle(AMQ::Protocol::Channel::FlowOk) do |connection, frame| channel = connection.channels[frame.channel] method = frame.decode_payload channel.flow_is_active = method.active channel.exec_callback(:flow, method) end self.handle(AMQ::Protocol::Tx::SelectOk) do |connection, frame| channel = connection.channels[frame.channel] channel.exec_callback(:tx_select, frame.decode_payload) end self.handle(AMQ::Protocol::Tx::CommitOk) do |connection, frame| channel = connection.channels[frame.channel] channel.exec_callback(:tx_commit, frame.decode_payload) end self.handle(AMQ::Protocol::Tx::RollbackOk) do |connection, frame| channel = connection.channels[frame.channel] channel.exec_callback(:tx_rollback, frame.decode_payload) end self.handle(AMQ::Protocol::Confirm::SelectOk) do |connection, frame| method = frame.decode_payload channel = connection.channels[frame.channel] channel.handle_select_ok(method) end self.handle(AMQ::Protocol::Basic::Ack) do |connection, frame| method = frame.decode_payload channel = connection.channels[frame.channel] channel.handle_basic_ack(method) end self.handle(AMQ::Protocol::Basic::Nack) do |connection, frame| method = frame.decode_payload channel = connection.channels[frame.channel] channel.handle_basic_nack(method) end protected @private def validate_parameters_match!(entity, parameters, type) unless entity.opts.values_at(*@parameter_checks[type]) == parameters.values_at(*@parameter_checks[type]) || parameters[:passive] raise AMQP::IncompatibleOptionsError.new(entity.name, entity.opts, parameters) end end # validate_parameters_match!(entity, parameters, type) end # Channel end # AMQP amqp-1.8.0/lib/amqp/header.rb0000644000004100000410000000505313321132064015765 0ustar www-datawww-data# encoding: utf-8 module AMQP # Message metadata (aka envelope). class Header # # API # # @api public # @return [AMQP::Channel] attr_reader :channel # AMQP method frame this header is associated with. # Carries additional information that varies between AMQP methods. # # @api public # @return [AMQ::Protocol::Method] attr_reader :method # AMQP message attributes # @return [Hash] attr_reader :attributes # @api public def initialize(channel, method, attributes) @channel, @method, @attributes = channel, method, attributes end # Acknowledges the receipt of this message with the server. # @param [Boolean] multiple Whether or not to acknowledge multiple messages # @api public def ack(multiple = false) @channel.acknowledge(@method.delivery_tag, multiple) end # Reject this message. # @option opts [Hash] :requeue (false) Whether message should be requeued. # @api public def reject(opts = {}) @channel.reject(@method.delivery_tag, opts.fetch(:requeue, false)) end # @return [Hash] AMQP message header w/o method-specific information. # @api public def to_hash @attributes end # to_hash def delivery_tag @method.delivery_tag end # delivery_tag def consumer_tag @method.consumer_tag end # consumer_tag def redelivered @method.redelivered end # redelivered def redelivered? @method.redelivered end # redelivered? def exchange @method.exchange end # exchange # @deprecated def header @attributes end # header def headers @attributes[:headers] end # headers def delivery_mode @attributes[:delivery_mode] end # delivery_mode def content_type @attributes[:content_type] end # content_type def timestamp @attributes[:timestamp] end # timestamp def type @attributes[:type] end # type def priority @attributes[:priority] end # priority def reply_to @attributes[:reply_to] end # reply_to def correlation_id @attributes[:correlation_id] end # correlation_id def message_id @attributes[:message_id] end # message_id # Returns AMQP message attributes. # @api public def method_missing(meth, *args, &blk) if @attributes && args.empty? && blk.nil? && @attributes.has_key?(meth) @attributes[meth] else @method.__send__(meth, *args, &blk) end end end # Header end # AMQP amqp-1.8.0/lib/amqp/handlers_registry.rb0000644000004100000410000000054713321132064020270 0ustar www-datawww-datamodule AMQP class HandlersRegistry @@handlers ||= Hash.new # # API # def self.register(klass, &block) @@handlers[klass] = block end class << self alias handle register end def self.find(klass) @@handlers[klass] end def self.handlers @@handlers end end # HandlersRegistry end amqp-1.8.0/lib/amqp/openable.rb0000644000004100000410000000166713321132064016331 0ustar www-datawww-datamodule AMQP module Openable VALUES = [:opened, :closed, :opening, :closing].freeze class ImproperStatusError < ArgumentError def initialize(value) super("Value #{value.inspect} isn't permitted. Choose one of: #{AMQP::Openable::VALUES.inspect}") end end attr_reader :status def status=(value) if VALUES.include?(value) @status = value else raise ImproperStatusError.new(value) end end def opened? @status == :opened end alias open? opened? def closed? @status == :closed end def opening? @status == :opening end def closing? @status == :closing end def opened! @status = :opened end # opened! def closed! @status = :closed end # closed! def opening! @status = :opening end # opening! def closing! @status = :closing end # closing! end end amqp-1.8.0/lib/amqp/utilities/0000755000004100000410000000000013321132064016220 5ustar www-datawww-dataamqp-1.8.0/lib/amqp/utilities/event_loop_helper.rb0000644000004100000410000000713713321132064022266 0ustar www-datawww-data# encoding: utf-8 require "eventmachine" require "amqp/utilities/server_type" require "thread" module AMQP module Utilities # A helper that starts EventMachine reactor the optimal way depending on what Web server # (if any) you are running. It should not be considered a 100% safe, general purpose EventMachine # reactor "on/off switch" but is very useful in Web applications and some stand-alone applications. # # This helper was inspired by Qusion project by Dan DeLeo. # # h2. Key methods # # * {EventLoopHelper.run} # * {EventLoopHelper.server_type} class EventLoopHelper def self.eventmachine_thread @eventmachine_thread end # self.eventmachine_thread def self.reactor_running? EventMachine.reactor_running? end # self.reactor_running? # Type of server (if any) that is running. # # @see AMQP::Utilities::ServerType.detect def self.server_type @server_type ||= ServerType.detect end # self.server_type # A helper that detects what app server (if any) is running and starts # EventMachine reactor in the most optimal way. For event-driven servers like # Thin and Goliath, this means relying on them starting the reactor but delaying # execution of a block you pass to {EventLoopHelper.run} until reactor is actually running. # # For Unicorn, Passenger, Mongrel and other servers and standalone apps EventMachine is started # in a separate thread. # # @example Using EventLoopHelper.run to start EventMachine reactor the optimal way without blocking current thread # # AMQP::Utilities::EventLoopHelper.run do # # Sets up default connection, accessible via AMQP.connection, and opens a channel # # accessible via AMQP.channel for convenience # AMQP.start # # exchange = AMQP.channel.fanout("amq.fanout") # # AMQP.channel.queue("", :auto_delete => true, :exclusive => true).bind(exchange) # AMQP::channel.default_exchange.publish("Started!", :routing_key => AMQP::State.queue.name) # end # # @return [Thread] A thread EventMachine event loop will be started in (there is no guarantee it is already running). # # # @note This method, unlike EventMachine.run, DOES NOT block current thread. def self.run(&block) if reactor_running? EventMachine.run(&block) return end @eventmachine_thread ||= begin case self.server_type when :thin, :goliath, :evented_mongrel then EventMachine.next_tick { block.call } Thread.current when :unicorn, :passenger, :mongrel, :scgi, :webrick, nil then t = Thread.new { EventMachine.run(&block) } # give EventMachine reactor some time to start sleep(0.25) t else t = Thread.new { EventMachine.run(&block) } # give EventMachine reactor some time to start sleep(0.25) t end end @eventmachine_thread end # self.run end # EventLoopHelper end # Utilities end # AMQP amqp-1.8.0/lib/amqp/utilities/server_type.rb0000644000004100000410000000475313321132064021125 0ustar www-datawww-data# encoding: utf-8 # Original version is from Qusion project by Daniel DeLeo. # # Copyright (c) 2009 Daniel DeLeo # Copyright (c) 2011 Michael Klishin # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. module AMQP module Utilities # A helper that detects Web server that may be running (if any). Partially derived # from Qusion project by Daniel DeLeo. class ServerType # Return a symbol representing Web server that is running (if any). # # Possible values are: # # * :thin for Thin # * :unicorn for Unicorn # * :passenger for Passenger (Apache mod_rack) # * :goliath for PostRank's Goliath # * :evented_mongrel for Swiftiply's Evented Mongrel # * :mongrel for Mongrel # * :scgi for SCGI # * :webrick for WEBrick # * nil: none of the above (the case for non-Web application, for example) # # @return [Symbol] def self.detect if defined?(::PhusionPassenger) :passenger elsif defined?(::Unicorn) :unicorn elsif defined?(::Thin) :thin elsif defined?(::Goliath) :goliath elsif defined?(::Mongrel) && defined?(::Mongrel::MongrelProtocol) :evented_mongrel elsif defined?(::Mongrel) :mongrel elsif defined?(::SCGI) :scgi elsif defined?(::WEBrick) :webrick else nil end # if end # self.detect end # ServerType end # Utilities end # AMQP amqp-1.8.0/lib/amqp/auth_mechanism_adapter.rb0000644000004100000410000000415413321132064021223 0ustar www-datawww-data# encoding: utf-8 module AMQP # Provides a flexible method for encoding AMQP credentials. PLAIN and # EXTERNAL are provided by this gem. In order to implement a new # authentication mechanism, create a subclass like so: # # class MyAuthMechanism < AMQP::Async::AuthMechanismAdapter # auth_mechanism "X-MYAUTH" # # def encode_credentials(username, password) # # ... # end # end class AuthMechanismAdapter # Find and instantiate an AuthMechanismAdapter. # # @param [Adapter] adapter The Adapter for which to encode credentials. # @return [AuthMechanismAdapter] An AuthMechanismAdapter that can encode # credentials for the Adapter. # @raise [NotImplementedError] The Adapter's mechanism does not # correspond to any known AuthMechanismAdapter. def self.for_adapter(adapter) registry[adapter.mechanism].new adapter end protected # Used by subclasses to declare the mechanisms that an # AuthMechanismAdapter understands. # # @param [Array] mechanisms One or more mechanisms that can be # handled by the subclass. def self.auth_mechanism(*mechanisms) mechanisms.each {|mechanism| registry[mechanism] = self} end private # Accesses the registry of AuthMechanismAdapter subclasses. Keys in # this hash are the names of the authentication mechanisms; values are # the classes that handle them. Referencing an unknown mechanism from # this Hash will raise NotImplementedError. def self.registry @@registry ||= Hash.new {raise NotImplementedError} end public # The Adapter that this AuthMechanismAdapter operates on behalf of. attr_reader :adapter private # Create a new AuthMechanismAdapter. Users of this class should instead # retrieve an instance through for_adapter. def initialize(adapter) @adapter = adapter end end end # pre-require builtin auth mechanisms Dir[File.join(File.dirname(__FILE__), File.basename(__FILE__, '.rb'), '*')].each do |f| require f end amqp-1.8.0/lib/amqp/deferrable.rb0000644000004100000410000000012713321132064016625 0ustar www-datawww-datarequire "eventmachine" module AMQP Deferrable = EventMachine::DefaultDeferrable end amqp-1.8.0/lib/amqp/queue.rb0000644000004100000410000015255113321132064015667 0ustar www-datawww-data# encoding: utf-8 require "amqp/entity" require "amq/protocol/get_response" require "amqp/consumer" module AMQP # h2. What are AMQP queues? # # Queues store and forward messages to consumers. They are similar to mailboxes in SMTP. # Messages flow from producing applications to {Exchange exchanges} that route them # to queues and finally queues deliver them to consumer applications (or consumer # applications fetch messages as needed). # # Note that unlike some other messaging protocols/systems, messages are not delivered directly # to queues. They are delivered to exchanges that route messages to queues using rules # knows as *bindings*. # # # h2. Concept of bindings # # Binding is an association between a queue and an exchange. # Queues must be bound to at least one exchange in order to receive messages from publishers. # Learn more about bindings in {Exchange Exchange class documentation}. # # # h2. Key methods # # Key methods of Queue class are # # * {Queue#bind} # * {Queue#subscribe} # * {Queue#pop} # * {Queue#delete} # * {Queue#purge} # * {Queue#unbind} # # # h2. Queue names. Server-named queues. Predefined queues. # # Every queue has a name that identifies it. Queue names often contain several segments separated by a dot (.), similarly to how URI # path segments are separated by a slash (/), although it may be almost any string, with some limitations (see below). # Applications may pick queue names or ask broker to generate a name for them. To do so, pass *empty string* as queue name argument. # # Here is an example: # # # # If you want to declare a queue with a particular name, for example, "images.resize", pass it to Queue class constructor: # # # # Queue names starting with 'amq.' are reserved for internal use by the broker. Attempts to declare queue with a name that violates this # rule will result in AMQP::IncompatibleOptionsError to be thrown (when # queue is re-declared on the same channel object) or channel-level exception (when originally queue # was declared on one channel and re-declaration with different attributes happens on another channel). # Learn more in {file:docs/Queues.textile Queues guide} and {file:docs/ErrorHandling.textile Error Handling guide}. # # # # h2. Queue life-cycles. When use of server-named queues is optimal and when it isn't. # # To quote AMQP 0.9.1 spec, there are two common message queue life-cycles: # # * Durable message queues that are shared by many consumers and have an independent existence: i.e. they # will continue to exist and collect messages whether or not there are consumers to receive them. # * Temporary message queues that are private to one consumer and are tied to that consumer. When the # consumer disconnects, the message queue is deleted. # # There are some variations on these, such as shared message queues that are deleted when the last of # many consumers disconnects. # # One example of durable message queues is well-known services like event collectors (event loggers). # They are usually up whether there are services to log anything or not. Other applications know what # queues they use and can rely on those queues being around all the time, survive broker restarts and # in general be available should an application in the network need to use them. In this case, # explicitly named durable queues are optimal and coupling it creates between applications is not # an issue. Another scenario of a well-known long-lived service is distributed metadata/directory/locking server # like Apache Zookeeper, Google's Chubby or DNS. Services like this benefit from using well-known, not generated # queue names, and so do other applications that use them. # # Different scenario is in "a cloud settings" when some kind of workers/instances may come online and # go down basically any time and other applications cannot rely on them being available. Using well-known # queue names in this case is possible but server-generated, short-lived queues that are bound to # topic or fanout exchanges to receive relevant messages is a better idea. # # Imagine a service that processes an endless stream of events (Twitter is one example). When traffic goes # up, development operations may spin up additional applications instances in the cloud to handle the load. # Those new instances want to subscribe to receive messages to process but the rest of the system doesn't # know anything about them, rely on them being online or try to address them directly: they process events # from a shared stream and are not different from their peers. In a case like this, there is no reason for # message consumers to not use queue names generated by the broker. # # In general, use of explicitly named or server-named queues depends on messaging pattern your application needs. # {http://www.eaipatterns.com/ Enterprise Integration Patters} discusses many messaging patterns in depth. # RabbitMQ FAQ also has a section on {http://www.rabbitmq.com/faq.html#scenarios use cases}. # # # h2. Queue durability and persistence of messages. # # Learn more in our {http://rubyamqp.info/articles/durability/}. # # # h2. Message ordering # # RabbitMQ FAQ explains {http://www.rabbitmq.com/faq.html#message-ordering ordering of messages in AMQP queues} # # # h2. Error handling # # When channel-level error occurs, queues associated with that channel are reset: internal state and callbacks # are cleared. Recommended strategy is to open a new channel and re-declare all the entities you need. # Learn more in {file:docs/ErrorHandling.textile Error Handling guide}. # # # @note Please make sure you read {http://rubyamqp.info/articles/durability/} that covers exchanges durability vs. messages # persistence. # # # @see https://www.rabbitmq.com/resources/specs/amqp0-9-1.pdf AMQP 0.9.1 specification (Section 2.1.1) # @see AMQP::Exchange class Queue # # Behaviours # include Entity include ServerNamedEntity extend ProtocolMethodHandlers # # API # # Name of this queue attr_reader :name # Options this queue object was instantiated with attr_accessor :opts # Channel this queue belongs to. # @return [AMQP::Channel] attr_reader :channel # @return [Array] All consumers on this queue. attr_reader :consumers # @return [AMQP::Consumer] Default consumer (registered with {Queue#consume}). attr_reader :default_consumer # @return [Hash] Additional arguments given on queue declaration. Typically used by AMQP extensions. attr_reader :arguments # @return [Array] attr_reader :bindings # @option opts [Boolean] :passive (false) If set, the server will not create the queue if it does not # already exist. The client can use this to check whether the queue # exists without modifying the server state. # # @option opts [Boolean] :durable (false) 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 (though it is # allowed). # # @option opts [Boolean] :exclusive (false) Exclusive queues may only be consumed from by the current connection. # Setting the 'exclusive' flag always implies 'auto-delete'. Only a # single consumer is allowed to remove messages from this queue. # The default is a shared queue. Multiple clients may consume messages # from this queue. # # @option opts [Boolean] :auto_delete (false) If set, the queue is deleted when all consumers have finished # using it. 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. # # @option opts [Boolean] :nowait (true) 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. # # # @option opts [Hash] :arguments (nil) A hash of optional arguments with the declaration. Some brokers implement # AMQP extensions using x-prefixed declaration arguments. For example, RabbitMQ # recognizes x-message-ttl declaration arguments that defines TTL of messages in # the queue. # # # @yield [queue, declare_ok] Yields successfully declared queue instance and AMQP method (queue.declare-ok) instance. The latter is optional. # @yieldparam [Queue] queue Queue that is successfully declared and is ready to be used. # @yieldparam [AMQ::Protocol::Queue::DeclareOk] declare_ok AMQP queue.declare-ok) instance. # # @api public def initialize(channel, name = AMQ::Protocol::EMPTY_STRING, opts = {}, &block) raise ArgumentError.new("queue name must not be nil; if you want broker to generate queue name for you, pass an empty string") if name.nil? @channel = channel @name = name unless name.empty? @server_named = name.empty? @opts = self.class.add_default_options(name, opts, block) raise ArgumentError.new("server-named queues (name = '') declaration with :nowait => true makes no sense. If you are not sure what that means, simply drop :nowait => true from opts.") if @server_named && @opts[:nowait] # a deferrable that we use to delay operations until this queue is actually declared. # one reason for this is to support a case when a server-named queue is immediately bound. # it's crazy, but 0.7.x supports it, so... MK. @declaration_deferrable = AMQP::Deferrable.new super(channel.connection) @name = name # this has to stay true even after queue.declare-ok arrives. MK. @server_named = @name.empty? if @server_named self.on_connection_interruption do # server-named queue need to get new names after recovery. MK. @name = AMQ::Protocol::EMPTY_STRING end end @channel = channel # primarily for autorecovery. MK. @bindings = Array.new @consumers = Hash.new shim = Proc.new do |q, declare_ok| case block.arity when 1 then block.call(q) else block.call(q, declare_ok) end end @channel.once_open do if @opts[:nowait] @declaration_deferrable.succeed block.call(self) if block end if block self.queue_declare(@opts[:passive], @opts[:durable], @opts[:exclusive], @opts[:auto_delete], @opts[:nowait], @opts[:arguments], &shim) else # we cannot pass :nowait as true here, AMQP::Queue will (rightfully) raise an exception because # it has no idea about crazy edge cases we are trying to support for sake of backwards compatibility. MK. self.queue_declare(@opts[:passive], @opts[:durable], @opts[:exclusive], @opts[:auto_delete], false, @opts[:arguments]) end end end # Defines a callback that will be executed once queue is declared. More than one callback can be defined. # if queue is already declared, given callback is executed immediately. # # @api public def once_declared(&block) @declaration_deferrable.callback do # guards against cases when deferred operations # don't complete before the channel is closed block.call if @channel.open? end end # once_declared(&block) # @return [Boolean] true if this queue was declared as durable (will survive broker restart). # @api public def durable? @durable end # durable? # @return [Boolean] true if this queue was declared as exclusive (limited to just one consumer) # @api public def exclusive? @exclusive end # exclusive? # @return [Boolean] true if this queue was declared as automatically deleted (deleted as soon as last consumer unbinds). # @api public def auto_delete? @auto_delete end # auto_delete? # @return [Boolean] true if this queue is server-named def server_named? @server_named end # server_named? # 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 dest exchange # and subscription queues are bound to a dest_wild exchange. # # A valid exchange name (or reference) must be passed as the first # parameter. # @example Binding a queue to exchange using AMQP::Exchange instance # # ch = AMQP::Channel.new(connection) # exchange = ch.direct('backlog.events') # queue = ch.queue('', :exclusive => true) # queue.bind(exchange) # # # @example Binding a queue to exchange using exchange name # # ch = AMQP::Channel.new(connection) # queue = ch.queue('', :exclusive => true) # queue.bind('backlog.events') # # # Note that if your producer application knows consumer queue name and wants to deliver # a message there, direct exchange may be sufficient (in other words, if your code declares an exchange with # the same name as a queue and binds it to that queue, consider using the default exchange and routing key on publishing). # # @param [Exchange] Exchange to bind to. May also be a string or any object that responds to #name. # # @option opts [String] :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 routing key is empty and the queue # name is empty, the routing key will be the current queue for the # channel, which is the last declared queue. # # @option opts [Hash] :arguments (nil) A hash of optional arguments with the declaration. Headers exchange type uses these metadata # attributes for routing matching. # In addition, brokers may implement AMQP extensions using x-prefixed declaration arguments. # # @option opts [Boolean] :nowait (true) 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. # @return [Queue] Self # # # @yield [] Since queue.bind-ok carries no attributes, no parameters are yielded to the block. # # @api public # @see Queue#unbind def bind(exchange, opts = {}, &block) @channel.once_open do self.once_name_is_available do queue_bind(exchange, (opts[:key] || opts[:routing_key] || AMQ::Protocol::EMPTY_STRING), (opts[:nowait] || block.nil?), opts[:arguments], &block) end end self end # Remove the binding between the queue and exchange. The queue will # not receive any more messages until it is bound to another # exchange. # # Due to the asynchronous nature of the protocol, it is possible for # "in flight" messages to be received after this call completes. # Those messages will be serviced by the last block used in a # {Queue#subscribe} or {Queue#pop} call. # # @param [Exchange] Exchange to unbind from. # @option opts [String] :routing_key Binding routing key # @option opts [Hash] :arguments Binding arguments # @option opts [Boolean] :nowait (true) 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. # # # @yield [] Since queue.unbind-ok carries no attributes, no parameters are yielded to the block. # # @api public # @see Queue#bind def unbind(exchange, opts = {}, &block) @channel.once_open do self.once_name_is_available do queue_unbind(exchange, (opts[:key] || opts[:routing_key] || AMQ::Protocol::EMPTY_STRING), opts[:arguments], &block) end end end # 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. # # @return [NilClass] nil (for v0.7 compatibility) # # @option opts [Boolean] :if_unused (false) 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. # # @option opts [Boolean] :if_empty (false) If set, the server will only delete the queue if it has no # messages. If the queue is not empty the server raises a channel # exception. # # @option opts [Boolean] :nowait (false) 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. # # # @return [NilClass] nil (for v0.7 compatibility) # # @yield [delete_ok] Yields AMQP method (queue.delete-ok) instance. # @yieldparam [AMQ::Protocol::Queue::DeleteOk] delete_ok AMQP queue.delete-ok) instance. Carries number of messages that were in the queue. # # @api public # @see Queue#purge # @see Queue#unbind def delete(opts = {}, &block) @channel.once_open do self.once_name_is_available do queue_delete(opts.fetch(:if_unused, false), opts.fetch(:if_empty, false), opts.fetch(:nowait, false), &block) end end # backwards compatibility nil end # This method removes all messages from a queue which are not awaiting acknowledgment. # # @option opts [Boolean] :nowait (false) 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. # # @return [NilClass] nil (for v0.7 compatibility) # # # @yield [purge_ok] Yields AMQP method (queue.purge-ok) instance. # @yieldparam [AMQ::Protocol::Queue::PurgeOk] purge_ok AMQP queue.purge-ok) instance. Carries number of messages that were purged. # # @api public # @see Queue#delete # @see Queue#unbind def purge(opts = {}, &block) @channel.once_open do self.once_declared do queue_purge(opts.fetch(:nowait, false), &block) end end # backwards compatibility nil end # 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. # # If queue is empty, `payload` callback argument will be nil, otherwise arguments # are identical to those of {AMQP::Queue#subscribe} callback. # # @example Fetching messages off AMQP queue on demand # # queue.pop do |metadata, payload| # if payload # puts "Fetched a message: #{payload.inspect}, content_type: #{metadata.content_type}. Shutting down..." # else # puts "No messages in the queue" # end # end # # @option opts [Boolean] :ack (false) If this field is set to false the server does not expect acknowledgments # for messages. That is, when a message is delivered to the client # the server automatically and silently acknowledges it on behalf # of the client. This functionality increases performance but at # the cost of reliability. Messages can get lost if a client dies # before it can deliver them to the application. # # # @return [Qeueue] Self # # # @yield [headers, payload] When block only takes one argument, yields payload to it. In case of two arguments, yields headers and payload. # @yieldparam [AMQP::Header] headers Headers (metadata) associated with this message (for example, routing key). # @yieldparam [String] payload Message body (content). On Ruby 1.9, you may want to check or enforce content encoding. # # @api public def pop(opts = {}, &block) if block # We have to maintain this multiple arities jazz # because older versions this gem are used in examples in at least 3 # books published by O'Reilly :(. MK. shim = Proc.new { |method, headers, payload| case block.arity when 1 then block.call(payload) when 2 then h = Header.new(@channel, method, headers ? headers.decode_payload : nil) block.call(h, payload) else h = Header.new(@channel, method, headers ? headers.decode_payload : nil) block.call(h, payload, method.delivery_tag, method.redelivered, method.exchange, method.routing_key) end } @channel.once_open do self.once_name_is_available do # see AMQP::Queue#get in amq-client self.get(!opts.fetch(:ack, false), &shim) end end else @channel.once_open do self.once_name_is_available do self.get(!opts.fetch(:ack, false)) end end end end # Subscribes to asynchronous message delivery. # # The provided block is passed a single message each time the # exchange matches a message to this queue. # # # Attempts to {Queue#subscribe} multiple times to the same exchange will raise an # Exception. If you need more than one consumer per queue, use {AMQP::Consumer} instead. # {file:docs/Queues.textile Documentation guide on queues} explains this and other topics # in great detail. # # # @example Use of callback with a single argument # # EventMachine.run do # exchange = AMQP::Channel.direct("foo queue") # EM.add_periodic_timer(1) do # exchange.publish("random number #{rand(1000)}") # end # # queue = AMQP::Channel.queue('foo queue') # queue.subscribe { |body| puts "received payload [#{body}]" } # end # # If the block takes 2 parameters, both the header and the body will # be passed in for processing. # # @example Use of callback with two arguments # # EventMachine.run do # connection = AMQP.connect(:host => '127.0.0.1') # puts "Connected to AMQP broker. Running #{AMQP::VERSION} version of the gem..." # # channel = AMQP::Channel.new(connection) # queue = channel.queue("amqpgem.examples.hello_world", :auto_delete => true) # exchange = channel.direct("amq.direct") # # queue.bind(exchange) # # channel.on_error do |ch, channel_close| # puts channel_close.reply_text # connection.close { EventMachine.stop } # end # # queue.subscribe do |metadata, payload| # puts "metadata.routing_key : #{metadata.routing_key}" # puts "metadata.content_type: #{metadata.content_type}" # puts "metadata.priority : #{metadata.priority}" # puts "metadata.headers : #{metadata.headers.inspect}" # puts "metadata.timestamp : #{metadata.timestamp.inspect}" # puts "metadata.type : #{metadata.type}" # puts "metadata.delivery_tag: #{metadata.delivery_tag}" # puts "metadata.redelivered : #{metadata.redelivered}" # # puts "metadata.app_id : #{metadata.app_id}" # puts "metadata.exchange : #{metadata.exchange}" # puts # puts "Received a message: #{payload}. Disconnecting..." # # connection.close { # EventMachine.stop { exit } # } # end # # exchange.publish("Hello, world!", # :app_id => "amqpgem.example", # :priority => 8, # :type => "kinda.checkin", # # headers table keys can be anything # :headers => { # :coordinates => { # :latitude => 59.35, # :longitude => 18.066667 # }, # :participants => 11, # :venue => "Stockholm" # }, # :timestamp => Time.now.to_i) # end # # @example Using object as consumer (message handler), take one # # class Consumer # # # # # API # # # # def initialize(channel, queue_name = AMQ::Protocol::EMPTY_STRING) # @queue_name = queue_name # # @channel = channel # # Consumer#handle_channel_exception will handle channel # # exceptions. Keep in mind that you can only register one error handler, # # so the last one registered "wins". # @channel.on_error(&method(:handle_channel_exception)) # end # initialize # # def start # @queue = @channel.queue(@queue_name, :exclusive => true) # # #handle_message method will be handling messages routed to @queue # @queue.subscribe(&method(:handle_message)) # end # start # # # # # # # Implementation # # # # def handle_message(metadata, payload) # puts "Received a message: #{payload}, content_type = #{metadata.content_type}" # end # handle_message(metadata, payload) # # def handle_channel_exception(channel, channel_close) # puts "Oops... a channel-level exception: code = #{channel_close.reply_code}, message = #{channel_close.reply_text}" # end # handle_channel_exception(channel, channel_close) # end # # # @example Using object as consumer (message handler), take two: aggregatied handler # class Consumer # # # # # API # # # # def handle_message(metadata, payload) # puts "Received a message: #{payload}, content_type = #{metadata.content_type}" # end # handle_message(metadata, payload) # end # # # class Worker # # # # # API # # # # # def initialize(channel, queue_name = AMQ::Protocol::EMPTY_STRING, consumer = Consumer.new) # @queue_name = queue_name # # @channel = channel # @channel.on_error(&method(:handle_channel_exception)) # # @consumer = consumer # end # initialize # # def start # @queue = @channel.queue(@queue_name, :exclusive => true) # @queue.subscribe(&@consumer.method(:handle_message)) # end # start # # # # # # # Implementation # # # # def handle_channel_exception(channel, channel_close) # puts "Oops... a channel-level exception: code = #{channel_close.reply_code}, message = #{channel_close.reply_text}" # end # handle_channel_exception(channel, channel_close) # end # # @example Unit-testing objects that are used as consumers, RSpec style # # require "ostruct" # require "json" # # # RSpec example # describe Consumer do # describe "when a new message arrives" do # subject { described_class.new } # # let(:metadata) do # o = OpenStruct.new # # o.content_type = "application/json" # o # end # let(:payload) { JSON.encode({ :command => "reload_config" }) } # # it "does some useful work" do # # check preconditions here if necessary # # subject.handle_message(metadata, payload) # # # add your code expectations here # end # end # end # # # # @option opts [Boolean ]:ack (false) If this field is set to false the server does not expect acknowledgments # for messages. That is, when a message is delivered to the client # the server automatically and silently acknowledges it on behalf # of the client. This functionality increases performance but at # the cost of reliability. Messages can get lost if a client dies # before it can deliver them to the application. # # @option opts [Boolean] :nowait (false) 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. # # @option opts [#call] :confirm (nil) If set, this proc will be called when the server confirms subscription # to the queue with a basic.consume-ok message. Setting this option will # automatically set :nowait => false. This is required for the server # to send a confirmation. # # @option opts [Boolean] :exclusive (false) Request exclusive consumer access, meaning only this consumer can access the queue. # This is useful when you want a long-lived shared queue to be temporarily accessible by just # one application (or thread, or process). If application exclusive consumer is part of crashes # or loses network connection to the broker, channel is closed and exclusive consumer is thus cancelled. # # # @yield [headers, payload] When block only takes one argument, yields payload to it. In case of two arguments, yields headers and payload. # @yieldparam [AMQP::Header] headers Headers (metadata) associated with this message (for example, routing key). # @yieldparam [String] payload Message body (content). On Ruby 1.9, you may want to check or enforce content encoding. # # @return [Queue] Self # @api public # # @see file:docs/Queues.textile Documentation guide on queues # @see #unsubscribe # @see AMQP::Consumer def subscribe(opts = {}, &block) raise RuntimeError.new("This queue already has default consumer. Please instantiate AMQP::Consumer directly to register additional consumers.") if @default_consumer opts[:nowait] = false if (@on_confirm_subscribe = opts[:confirm]) @channel.once_open do self.once_name_is_available do # guards against a pathological case race condition when a channel # is opened and closed before delayed operations are completed. self.basic_consume(!opts[:ack], opts[:exclusive], (opts[:nowait] || block.nil?), opts[:no_local], nil, &opts[:confirm]) self.on_delivery(&block) end end self end # @api public # @see https://www.rabbitmq.com/resources/specs/amqp-xml-doc0-9-1.pdf AMQP 0.9.1 protocol reference (Sections 1.8.3.9) def on_delivery(&block) @default_consumer.on_delivery(&block) end # on_delivery(&block) # @return [String] Consumer tag of the default consumer associated with this queue (if any), or nil # @note Default consumer is the one registered with the convenience {AMQP::Queue#subscribe} method. It has no special properties of any kind. # @see Queue#subscribe # @see AMQP::Consumer # @api public def consumer_tag if @default_consumer @default_consumer.consumer_tag else nil end end # consumer_tag # @return [AMQP::Consumer] Default consumer associated with this queue (if any), or nil # @note Default consumer is the one registered with the convenience {AMQP::Queue#subscribe} method. It has no special properties of any kind. # @see Queue#subscribe # @see AMQP::Consumer # @api public def default_consumer @default_consumer end # @return [Class] # @private def self.consumer_class AMQP::Consumer end # self.consumer_class # Removes the subscription from the queue and cancels the consumer. Once consumer is cancelled, # messages will no longer be delivered to it, however, due to the asynchronous nature of the protocol, it is possible for # “in flight” messages to be received after this call completes. # Those messages will be serviced by the last block used in a # {Queue#subscribe} or {Queue#pop} call. # # Fetching messages with {AMQP::Queue#pop} is still possible even after consumer is cancelled. # # Additionally, if the queue was created with _autodelete_ set to # true, the server will delete the queue after its wait period # has expired unless the queue is bound to an active exchange. # # The method accepts a block which will be executed when the # unsubscription request is acknowledged as complete by the server. # # @option opts [Boolean] :nowait (true) If set, the server will not respond to the method. The client should # not wait for a reply method, the callback (if passed) will be ignored. If the server could not complete the # method it will raise a channel or connection exception. # # @yield [cancel_ok] # @yieldparam [AMQ::Protocol::Basic::CancelOk] cancel_ok AMQP method basic.cancel-ok. You can obtain consumer tag from it. # # # @api public def unsubscribe(opts = {}, &block) @channel.once_open do self.once_name_is_available do if @default_consumer @default_consumer.cancel(opts.fetch(:nowait, true), &block); @default_consumer = nil end end end end # Get the number of messages and active consumers (with active channel flow) on a queue. # # @example Getting number of messages and active consumers for a queue # # AMQP::Channel.queue('name').status { |number_of_messages, number_of_active_consumers| # puts number_of_messages # } # # @yield [number_of_messages, number_of_active_consumers] # @yieldparam [Fixnum] number_of_messages Number of messages in the queue # @yieldparam [Fixnum] number_of_active_consumers 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. # # @api public def status(opts = {}, &block) raise ArgumentError, "AMQP::Queue#status does not make any sense without a block" unless block shim = Proc.new { |q, declare_ok| block.call(declare_ok.message_count, declare_ok.consumer_count) } @channel.once_open do self.once_name_is_available do # we do not use self.declare here to avoid caching of @passive since that will cause unexpected side-effects during automatic # recovery process. MK. @connection.send_frame(AMQ::Protocol::Queue::Declare.encode(@channel.id, @name, true, @opts[:durable], @opts[:exclusive], @opts[:auto_delete], false, @opts[:arguments])) self.append_callback(:declare, &shim) @channel.queues_awaiting_declare_ok.push(self) end end self end # Boolean check to see if the current queue has already subscribed # to messages delivery (has default consumer). # # Attempts to {Queue#subscribe} multiple times to the same exchange will raise an # Exception. If you need more than one consumer per queue, use {AMQP::Consumer} instead. # # @return [Boolean] true if there is a consumer tag associated with this Queue instance # @api public # @deprecated def subscribed? @default_consumer && @default_consumer.subscribed? end # Compatibility alias for #on_declare. # # @api public # @deprecated def callback return nil if !subscribed? @default_consumer.callback end # Don't use this method. It is a leftover from very early days and # it ruins the whole point of exchanges/queue separation. # # @note This method will be removed before 1.0 release # @deprecated # @api public def publish(data, opts = {}) exchange.publish(data, opts.merge(:routing_key => self.name)) end # Resets queue state. Useful for error handling. # @api plugin def reset initialize(@channel, @name, @opts) end # @private # @api plugin def handle_connection_interruption(method = nil) @consumers.each { |tag, consumer| consumer.handle_connection_interruption(method) } self.exec_callback_yielding_self(:after_connection_interruption) @declaration_deferrable = EventMachine::DefaultDeferrable.new end def handle_declare_ok(method) @name = method.queue if @name.empty? @channel.register_queue(self) self.exec_callback_once_yielding_self(:declare, method) @declaration_deferrable.succeed end # @group Declaration # Declares this queue. # # # @return [Queue] self # # @api public # @see https://www.rabbitmq.com/resources/specs/amqp-xml-doc0-9-1.pdf AMQP 0.9.1 protocol reference (Section 1.7.2.1.) def queue_declare(passive = false, durable = false, exclusive = false, auto_delete = false, nowait = false, arguments = nil, &block) raise ArgumentError, "declaration with nowait does not make sense for server-named queues! Either specify name other than empty string or use #declare without nowait" if nowait && self.anonymous? # these two are for autorecovery. MK. @passive = passive @server_named = @name.empty? @durable = durable @exclusive = exclusive @auto_delete = auto_delete @arguments = arguments nowait = true if !block && !@name.empty? && nowait.nil? @connection.send_frame(AMQ::Protocol::Queue::Declare.encode(@channel.id, @name, passive, durable, exclusive, auto_delete, nowait, arguments)) if !nowait self.append_callback(:declare, &block) @channel.queues_awaiting_declare_ok.push(self) end self end # Re-declares queue with the same attributes # @api public def redeclare(&block) nowait = true if !block && !@name.empty? # server-named queues get their new generated names. new_name = if @server_named AMQ::Protocol::EMPTY_STRING else @name end @connection.send_frame(AMQ::Protocol::Queue::Declare.encode(@channel.id, new_name, @passive, @durable, @exclusive, @auto_delete, false, @arguments)) if !nowait self.append_callback(:declare, &block) @channel.queues_awaiting_declare_ok.push(self) end self end # @endgroup # Deletes this queue. # # @param [Boolean] if_unused delete only if queue has no consumers (subscribers). # @param [Boolean] if_empty delete only if queue has no messages in it. # @param [Boolean] nowait Don't wait for reply from broker. # @return [Queue] self # # @api public # @see https://www.rabbitmq.com/resources/specs/amqp-xml-doc0-9-1.pdf AMQP 0.9.1 protocol reference (Section 1.7.2.9.) def queue_delete(if_unused = false, if_empty = false, nowait = false, &block) nowait = true unless block @connection.send_frame(AMQ::Protocol::Queue::Delete.encode(@channel.id, @name, if_unused, if_empty, nowait)) if !nowait self.append_callback(:delete, &block) # TODO: delete itself from queues cache @channel.queues_awaiting_delete_ok.push(self) end self end # delete(channel, queue, if_unused, if_empty, nowait, &block) # @group Binding # # @return [Queue] self # # @api public # @see https://www.rabbitmq.com/resources/specs/amqp-xml-doc0-9-1.pdf AMQP 0.9.1 protocol reference (Section 1.7.2.3.) def queue_bind(exchange, routing_key = AMQ::Protocol::EMPTY_STRING, nowait = false, arguments = nil, &block) nowait = true unless block exchange_name = if exchange.respond_to?(:name) exchange.name else exchange end @connection.send_frame(AMQ::Protocol::Queue::Bind.encode(@channel.id, @name, exchange_name, routing_key, nowait, arguments)) if !nowait self.append_callback(:bind, &block) @channel.queues_awaiting_bind_ok.push(self) end # store bindings for automatic recovery, but BE VERY CAREFUL to # not cause an infinite rebinding loop here when we recover. MK. binding = { :exchange => exchange_name, :routing_key => routing_key, :arguments => arguments } @bindings.push(binding) unless @bindings.include?(binding) self end # # @return [Queue] self # # @api public # @see https://www.rabbitmq.com/resources/specs/amqp-xml-doc0-9-1.pdf AMQP 0.9.1 protocol reference (Section 1.7.2.5.) def queue_unbind(exchange, routing_key = AMQ::Protocol::EMPTY_STRING, arguments = nil, &block) exchange_name = if exchange.respond_to?(:name) exchange.name else exchange end @connection.send_frame(AMQ::Protocol::Queue::Unbind.encode(@channel.id, @name, exchange_name, routing_key, arguments)) self.append_callback(:unbind, &block) @channel.queues_awaiting_unbind_ok.push(self) @bindings.delete_if { |b| b[:exchange] == exchange_name } self end # @endgroup # @group Consuming messages # # @return [Queue] self # # @api public # @see https://www.rabbitmq.com/resources/specs/amqp-xml-doc0-9-1.pdf AMQP 0.9.1 protocol reference (Section 1.8.3.3.) def basic_consume(no_ack = false, exclusive = false, nowait = false, no_local = false, arguments = nil, &block) raise RuntimeError.new("This queue already has default consumer. Please instantiate AMQP::Consumer directly to register additional consumers.") if @default_consumer nowait = true unless block @default_consumer = self.class.consumer_class.new(@channel, self, generate_consumer_tag(@name), exclusive, no_ack, arguments, no_local, &block) @default_consumer.consume(nowait, &block) self end # Unsubscribes from message delivery. # @return [Queue] self # # @api public # @see https://www.rabbitmq.com/resources/specs/amqp-xml-doc0-9-1.pdf AMQP 0.9.1 protocol reference (Section 1.8.3.5.) def cancel(nowait = false, &block) raise "There is no default consumer for this queue. This usually means that you are trying to unsubscribe a queue that never was subscribed for messages in the first place." if @default_consumer.nil? @default_consumer.cancel(nowait, &block) self end # cancel(&block) # @api public def on_cancel(&block) @default_consumer.on_cancel(&block) end # on_cancel(&block) # @endgroup # @group Working With Messages # Fetches messages from the queue. # @return [Queue] self # # @api public # @see https://www.rabbitmq.com/resources/specs/amqp-xml-doc0-9-1.pdf AMQP 0.9.1 protocol reference (Section 1.8.3.10.) def get(no_ack = false, &block) @connection.send_frame(AMQ::Protocol::Basic::Get.encode(@channel.id, @name, no_ack)) # most people only want one callback per #get call. Consider the following example: # # 100.times { queue.get { ... } } # # most likely you won't expect 100 callback runs per message here. MK. self.redefine_callback(:get, &block) @channel.queues_awaiting_get_response.push(self) self end # get(no_ack = false, &block) # Purges (removes all messagse from) the queue. # @return [Queue] self # # @api public # @see https://www.rabbitmq.com/resources/specs/amqp-xml-doc0-9-1.pdf AMQP 0.9.1 protocol reference (Section 1.7.2.7.) def queue_purge(nowait = false, &block) nowait = true unless block @connection.send_frame(AMQ::Protocol::Queue::Purge.encode(@channel.id, @name, nowait)) if !nowait self.redefine_callback(:purge, &block) # TODO: handle channel & connection-level exceptions @channel.queues_awaiting_purge_ok.push(self) end self end # purge(nowait = false, &block) # @endgroup # @group Acknowledging & Rejecting Messages # Acknowledge a delivery tag. # @return [Queue] self # # @api public # @see https://www.rabbitmq.com/resources/specs/amqp-xml-doc0-9-1.pdf AMQP 0.9.1 protocol reference (Section 1.8.3.13.) def acknowledge(delivery_tag) @channel.acknowledge(delivery_tag) self end # acknowledge(delivery_tag) # # @return [Queue] self # # @api public # @see https://www.rabbitmq.com/resources/specs/amqp-xml-doc0-9-1.pdf AMQP 0.9.1 protocol reference (Section 1.8.3.14.) def reject(delivery_tag, requeue = true) @channel.reject(delivery_tag, requeue) self end # reject(delivery_tag, requeue = true) # @endgroup # @group Error Handling & Recovery # Used by automatic recovery machinery. # @private # @api api def rebind(&block) @bindings.each { |b| self.bind(b[:exchange], b) } end # Called by associated connection object when AMQP connection has been re-established # (for example, after a network failure). # # @api api def auto_recover self.exec_callback_yielding_self(:before_recovery) if self.server_named? old_name = @name.dup @name = AMQ::Protocol::EMPTY_STRING @channel.queues.delete(old_name) end self.redeclare do self.rebind @consumers.each { |tag, consumer| consumer.auto_recover } self.exec_callback_yielding_self(:after_recovery) end end # auto_recover # Defines a callback that will be executed after TCP connection is interrupted (typically because of a network failure). # Only one callback can be defined (the one defined last replaces previously added ones). # # @api public def on_connection_interruption(&block) self.redefine_callback(:after_connection_interruption, &block) end # on_connection_interruption(&block) alias after_connection_interruption on_connection_interruption # Defines a callback that will be executed after TCP connection is recovered after a network failure # but before AMQP connection is re-opened. # Only one callback can be defined (the one defined last replaces previously added ones). # # @api public def before_recovery(&block) self.redefine_callback(:before_recovery, &block) end # before_recovery(&block) # @private def run_before_recovery_callbacks self.exec_callback_yielding_self(:before_recovery) @consumers.each { |tag, c| c.run_before_recovery_callbacks } end # Defines a callback that will be executed when AMQP connection is recovered after a network failure.. # Only one callback can be defined (the one defined last replaces previously added ones). # # @api public def on_recovery(&block) self.redefine_callback(:after_recovery, &block) end # on_recovery(&block) alias after_recovery on_recovery # @private def run_after_recovery_callbacks self.exec_callback_yielding_self(:after_recovery) @consumers.each { |tag, c| c.run_after_recovery_callbacks } end # @endgroup # # Implementation # # Unique string supposed to be used as a consumer tag. # # @return [String] Unique string. # @api plugin def generate_consumer_tag(name) "#{name}-#{Time.now.to_i * 1000}-#{Kernel.rand(999_999_999_999)}" end def handle_connection_interruption(method = nil) @consumers.each { |tag, c| c.handle_connection_interruption(method) } end # handle_connection_interruption(method = nil) def handle_delete_ok(method) self.exec_callback_once(:delete, method) end # handle_delete_ok(method) def handle_purge_ok(method) self.exec_callback_once(:purge, method) end # handle_purge_ok(method) def handle_bind_ok(method) self.exec_callback_once(:bind, method) end # handle_bind_ok(method) def handle_unbind_ok(method) self.exec_callback_once(:unbind, method) end # handle_unbind_ok(method) def handle_get_ok(method, header, payload) method = AMQ::Protocol::GetResponse.new(method) self.exec_callback(:get, method, header, payload) end # handle_get_ok(method, header, payload) def handle_get_empty(method) method = AMQ::Protocol::GetResponse.new(method) self.exec_callback(:get, method) end # handle_get_empty(method) # Get the first queue which didn't receive Queue.Declare-Ok yet and run its declare callback. # The cache includes only queues with {nowait: false}. self.handle(AMQ::Protocol::Queue::DeclareOk) do |connection, frame| method = frame.decode_payload channel = connection.channels[frame.channel] queue = channel.queues_awaiting_declare_ok.shift queue.handle_declare_ok(method) end self.handle(AMQ::Protocol::Queue::DeleteOk) do |connection, frame| channel = connection.channels[frame.channel] queue = channel.queues_awaiting_delete_ok.shift queue.handle_delete_ok(frame.decode_payload) end self.handle(AMQ::Protocol::Queue::BindOk) do |connection, frame| channel = connection.channels[frame.channel] queue = channel.queues_awaiting_bind_ok.shift queue.handle_bind_ok(frame.decode_payload) end self.handle(AMQ::Protocol::Queue::UnbindOk) do |connection, frame| channel = connection.channels[frame.channel] queue = channel.queues_awaiting_unbind_ok.shift queue.handle_unbind_ok(frame.decode_payload) end self.handle(AMQ::Protocol::Queue::PurgeOk) do |connection, frame| channel = connection.channels[frame.channel] queue = channel.queues_awaiting_purge_ok.shift queue.handle_purge_ok(frame.decode_payload) end self.handle(AMQ::Protocol::Basic::GetOk) do |connection, frame, content_frames| channel = connection.channels[frame.channel] queue = channel.queues_awaiting_get_response.shift method = frame.decode_payload header = content_frames.shift body = content_frames.map {|frame| frame.payload }.join queue.handle_get_ok(method, header, body) if queue end self.handle(AMQ::Protocol::Basic::GetEmpty) do |connection, frame| channel = connection.channels[frame.channel] queue = channel.queues_awaiting_get_response.shift queue.handle_get_empty(frame.decode_payload) end protected # @private def self.add_default_options(name, opts, block) { :queue => name, :nowait => (block.nil? && !name.empty?) }.merge(opts) end def once_name_is_available(&block) if server_named? self.once_declared do block.call end else block.call end end private # Default direct exchange that we use to publish messages directly to this queue. # This is a leftover from very early days and will be removed before version 1.0. # # @deprecated def exchange @exchange ||= Exchange.new(@channel, :direct, AMQ::Protocol::EMPTY_STRING, :key => name) end end # Queue end # AMQP amqp-1.8.0/lib/amqp/consumer.rb0000644000004100000410000002721013321132064016367 0ustar www-datawww-data# encoding: utf-8 require "amqp/consumer_tag_generator" module AMQP # AMQP consumers are entities that handle messages delivered to them ("push API" as opposed to "pull API") by AMQP broker. # Every consumer is associated with a queue. Consumers can be exclusive (no other consumers can be registered for the same queue) # or not (consumers share the queue). In the case of multiple consumers per queue, messages are distributed in round robin # manner with respect to channel-level prefetch setting). # # @see AMQP::Queue # @see AMQP::Queue#subscribe # @see AMQP::Queue#cancel class Consumer # # Behaviours # include Callbacks extend ProtocolMethodHandlers # # API # # @return [AMQP::Channel] Channel this consumer uses attr_reader :channel # @return [AMQP::Queue] Queue messages are consumed from attr_reader :queue # @return [String] Consumer tag, unique consumer identifier attr_reader :consumer_tag # @return [Hash] Custom subscription metadata attr_reader :arguments # @return [AMQP::ConsumerTagGenerator] Consumer tag generator def self.tag_generator @tag_generator ||= AMQP::ConsumerTagGenerator.new end # self.tag_generator # @param [AMQP::ConsumerTagGenerator] Assigns consumer tag generator that will be used by consumer instances # @return [AMQP::ConsumerTagGenerator] Provided argument def self.tag_generator=(generator) @tag_generator = generator end def initialize(channel, queue, consumer_tag = nil, exclusive = false, no_ack = false, arguments = {}, no_local = false, &block) @callbacks = Hash.new @channel = channel || raise(ArgumentError, "channel is nil") @connection = channel.connection || raise(ArgumentError, "connection is nil") @queue = queue || raise(ArgumentError, "queue is nil") @consumer_tag = consumer_tag || self.class.tag_generator.generate_for(queue) @exclusive = exclusive @no_ack = no_ack @arguments = arguments @no_local = no_local self.register_with_channel self.register_with_queue end # initialize # @return [Boolean] true if this consumer is exclusive (other consumers for the same queue are not allowed) def exclusive? !!@exclusive end # exclusive? # Begin consuming messages from the queue # @return [AMQP::Consumer] self def consume(nowait = false, &block) @channel.once_open do @queue.once_declared do @connection.send_frame(AMQ::Protocol::Basic::Consume.encode(@channel.id, @queue.name, @consumer_tag, @no_local, @no_ack, @exclusive, nowait, @arguments)) if !nowait self.redefine_callback(:consume, &block) @channel.consumers_awaiting_consume_ok.push(self) end self end end self end # consume(nowait = false, &block) # Used by automatic recovery code. # @api plugin # @return [AMQP::Consumer] self def resubscribe(&block) @channel.once_open do @queue.once_declared do self.unregister_with_channel @consumer_tag = self.class.tag_generator.generate_for(@queue) self.register_with_channel @connection.send_frame(AMQ::Protocol::Basic::Consume.encode(@channel.id, @queue.name, @consumer_tag, @no_local, @no_ack, @exclusive, block.nil?, @arguments)) self.redefine_callback(:consume, &block) if block self end end self end # resubscribe(&block) # @return [AMQP::Consumer] self def cancel(nowait = false, &block) @channel.once_open do @queue.once_declared do @connection.send_frame(AMQ::Protocol::Basic::Cancel.encode(@channel.id, @consumer_tag, nowait)) if !nowait self.redefine_callback(:cancel, &block) @channel.consumers_awaiting_cancel_ok.push(self) end self end end self end # cancel(nowait = false, &block) # {AMQP::Queue} API compatibility. # # @return [Boolean] true if this consumer is active (subscribed for message delivery) # @api public def subscribed? !@callbacks[:delivery].empty? end # subscribed? # Legacy {AMQP::Queue} API compatibility. # @private # @deprecated def callback if @callbacks[:delivery] @callbacks[:delivery].first end end # callback # Register a block that will be used to handle delivered messages. # # @return [AMQP::Consumer] self # @see AMQP::Queue#subscribe def on_delivery(&block) # We have to maintain this multiple arities jazz # because older versions this gem are used in examples in at least 3 # books published by O'Reilly :(. MK. delivery_shim = Proc.new { |basic_deliver, headers, payload| case block.arity when 1 then block.call(payload) when 2 then h = Header.new(@channel, basic_deliver, headers.decode_payload) block.call(h, payload) else h = Header.new(@channel, basic_deliver, headers.decode_payload) block.call(h, payload, basic_deliver.consumer_tag, basic_deliver.delivery_tag, basic_deliver.redelivered, basic_deliver.exchange, basic_deliver.routing_key) end } self.append_callback(:delivery, &delivery_shim) self end # on_delivery(&block) # @return [String] Readable representation of relevant object state. def inspect "# queue=#{@queue.name} channel=#{@channel.id} callbacks=#{@callbacks.inspect}" end # inspect def on_cancel(&block) self.append_callback(:scancel, &block) self end # on_cancel(&block) def handle_cancel(basic_cancel) self.exec_callback(:scancel, basic_cancel) end # handle_cancel(basic_cancel) # @group Acknowledging & Rejecting Messages # Acknowledge a delivery tag. # @return [Consumer] self # # @api public # @see http://bit.ly/htCzCX AMQP 0.9.1 protocol documentation (Section 1.8.3.13.) def acknowledge(delivery_tag) @channel.acknowledge(delivery_tag) self end # acknowledge(delivery_tag) # # @return [Consumer] self # # @api public # @see http://bit.ly/htCzCX AMQP 0.9.1 protocol documentation (Section 1.8.3.14.) def reject(delivery_tag, requeue = true) @channel.reject(delivery_tag, requeue) self end # reject(delivery_tag, requeue = true) # Defines a callback that will be executed when AMQP connection is recovered after a network failure.. # Only one callback can be defined (the one defined last replaces previously added ones). # # @api public # Defines a callback that will be executed when AMQP connection is recovered after a network failure.. # Only one callback can be defined (the one defined last replaces previously added ones). # # @api public def on_recovery(&block) self.redefine_callback(:after_recovery, &block) end # on_recovery(&block) alias after_recovery on_recovery # @endgroup # @group Error Handling & Recovery # Defines a callback that will be executed after TCP connection is interrupted (typically because of a network failure). # Only one callback can be defined (the one defined last replaces previously added ones). # # @api public def on_connection_interruption(&block) self.redefine_callback(:after_connection_interruption, &block) end # on_connection_interruption(&block) alias after_connection_interruption on_connection_interruption # @private def handle_connection_interruption(method = nil) self.exec_callback_yielding_self(:after_connection_interruption) end # handle_connection_interruption # Defines a callback that will be executed after TCP connection is recovered after a network failure # but before AMQP connection is re-opened. # Only one callback can be defined (the one defined last replaces previously added ones). # # @api public def before_recovery(&block) self.redefine_callback(:before_recovery, &block) end # before_recovery(&block) # @private def run_before_recovery_callbacks self.exec_callback_yielding_self(:before_recovery) end # @private def run_after_recovery_callbacks self.exec_callback_yielding_self(:after_recovery) end # Called by associated connection object when AMQP connection has been re-established # (for example, after a network failure). # # @api plugin def auto_recover self.exec_callback_yielding_self(:before_recovery) self.resubscribe self.exec_callback_yielding_self(:after_recovery) end # auto_recover # @endgroup def to_s "#<#{self.class.name} @consumer_tag=#{@consumer_tag} @queue=#{@queue.name} @channel=#{@channel.id}>" end # # Implementation # def handle_delivery(basic_deliver, metadata, payload) self.exec_callback(:delivery, basic_deliver, metadata, payload) end # handle_delivery(basic_deliver, metadata, payload) def handle_consume_ok(consume_ok) self.exec_callback_once(:consume, consume_ok) end # handle_consume_ok(consume_ok) def handle_cancel_ok(cancel_ok) self.exec_callback_once(:cancel, cancel_ok) self.unregister_with_channel self.unregister_with_queue @consumer_tag = nil # detach from object graph so that this object will be garbage-collected @queue = nil @channel = nil @connection = nil self.clear_callbacks(:delivery) self.clear_callbacks(:consume) self.clear_callbacks(:cancel) self.clear_callbacks(:scancel) end # handle_cancel_ok(method) self.handle(AMQ::Protocol::Basic::ConsumeOk) do |connection, frame| channel = connection.channels[frame.channel] consumer = channel.consumers_awaiting_consume_ok.shift consumer.handle_consume_ok(frame.decode_payload) end self.handle(AMQ::Protocol::Basic::CancelOk) do |connection, frame| channel = connection.channels[frame.channel] consumer = channel.consumers_awaiting_cancel_ok.shift consumer.handle_cancel_ok(frame.decode_payload) end self.handle(AMQ::Protocol::Basic::Deliver) do |connection, method_frame, content_frames| channel = connection.channels[method_frame.channel] basic_deliver = method_frame.decode_payload consumer = channel.consumers[basic_deliver.consumer_tag] metadata = content_frames.shift payload = content_frames.map { |frame| frame.payload }.join # Handle the delivery only if the consumer still exists. # The broker has been known to deliver a few messages after the consumer has been shut down. consumer.handle_delivery(basic_deliver, metadata, payload) if consumer end protected def register_with_channel @channel.consumers[@consumer_tag] = self end # register_with_channel def register_with_queue @queue.consumers[@consumer_tag] = self end # register_with_queue def unregister_with_channel @channel.consumers.delete(@consumer_tag) end # register_with_channel def unregister_with_queue @queue.consumers.delete(@consumer_tag) end # register_with_queue handle(AMQ::Protocol::Basic::Cancel) do |connection, method_frame| channel = connection.channels[method_frame.channel] basic_cancel = method_frame.decode_payload consumer = channel.consumers[basic_cancel.consumer_tag] # Handle the delivery only if the consumer still exists. consumer.handle_cancel(basic_cancel) if consumer end end # Consumer end # AMQP amqp-1.8.0/lib/amqp/exchange.rb0000644000004100000410000011242513321132064016321 0ustar www-datawww-data# encoding: utf-8 module AMQP # h2. What are AMQP exchanges? # # AMQP exchange is where AMQP clients send messages. AMQP # exchange may also be described as a router or a matcher. Every # published message is received by an exchange which, depending on its # type and message attributes, determines how to deliver the message. # # # Entities that forward messages to consumers (or consumers fetch messages # from on demand) are called {Queue queues}. Exchanges are associated with # queues via bindings. Roughly speaking, bindings determine messages placed # in what exchange end up in what queues. # # # h2. AMQP bindings # # Closely related to exchange is a concept of bindings. A binding is # the relationship between an exchange and a message queue that tells # the exchange how to route messages. Bindings are set up by # AMQP applications (usually the app owning and using the message queue # sets up bindings for it). Exchange may be bound to none, 1 or more than 1 # queue. # # # h2. Exchange types # # There are 4 supported exchange types: direct, fanout, topic and headers. # Exchange type determines how exchange processes and routes messages. # # # h2. Direct exchanges # # Direct exchanges are useful for 1:1 communication scenarios. # Queues are bound to direct exchanges with a parameter called "routing key". When messages # arrive to a direct exchange, broker takes that message's routing key (if any), finds a queue bound # to the exchange with the same routing key and routes message there. # # Because very often queues are bound with the same routing key as queue's name, AMQP 0.9.1 has # a pre-declared direct exchange known as default exchange. Default exchange is a bit special: broker # automatically binds all the queues (in the same virtual host) to it with routing key equal to # queue names. In other words, messages delivered to default exchange are routed to queues when # message routing key equals queue name. Default exchange name is an empty string. # # As part of the standard, the server _must_ predeclare the direct exchange # 'amq.direct' and the fanout exchange 'amq.fanout' (all exchange names # starting with 'amq.' are reserved). Attempts to declare an exchange using # 'amq.' as the name will result in a channel-level exception and fail. In practice these # default exchanges are never used directly by client code. # # # h2. Fanout exchanges # # Fanout exchanges are useful for 1:n and n:m communication where one or more producer # feeds multiple consumers. messages published # to a fanout exchange are delivered to queues that are bound to that exchange name (unconditionally). # Each queue gets it's own copy of the message. # # # h2. Topic exchanges # # Topic exchanges are used for 1:n and n:m communication scenarios. # Exchange of this type uses the routing key # to determine which queues to deliver the message. Wildcard matching # is allowed. The topic must be declared using dot notation to separate # each subtopic. # # As part of the AMQP standard, each server _should_ predeclare a topic # exchange called 'amq.topic'. # # The classic example is delivering market data. When publishing market # data for stocks, we may subdivide the stream based on 2 # characteristics: nation code and trading symbol. The topic tree for # Apple may look like stock.us.aapl. NASDAQ updates may use topic stocks.us.nasdaq, # while DAX may use stock.de.dax. # # When publishing data to the exchange, bound queues subscribing to the # exchange indicate which data interests them by passing a routing key # for matching against the published routing key. # # # h2. Headers exchanges # # When publishing data to exchange of type headers, bound queues subscribing to the # exchange indicate which data interests them by passing arguments # for matching against the headers in published messages. The # form of the matching can be controlled by the 'x-match' argument, which # may be 'any' or 'all'. If unspecified, it defaults to "all". # # A value of 'all' for 'x-match' implies that all values must match (i.e. # it does an AND of the headers ), while a value of 'any' implies that # at least one should match (ie. it does an OR). # # As part of the AMQP standard, each server _should_ predeclare a headers # exchange named 'amq.match'. # # # h2. Key methods # # Key methods of Exchange class are # # * {Exchange#publish} # * {Exchange#delete} # * {Exchange.default} # # # # h2. Exchange durability and persistence of messages. # # Learn more in our {file:docs/Durability.textile Durability guide}. # # # # h2. RabbitMQ extensions. # # AMQP gem supports several RabbitMQ extensions taht extend Exchange functionality. # Learn more in {file:docs/VendorSpecificExtensions.textile} # # # # @note Please make sure you read a section on exchanges durability vs. messages # persistence. # # @see http://www.rabbitmq.com/faq.html#managing-concepts-exchanges Exchanges explained in the RabbitMQ FAQ # @see http://www.rabbitmq.com/faq.html#Binding-and-Routing Bindings and routing explained in the RabbitMQ FAQ # @see Channel#default_exchange # @see Channel#direct # @see Channel#fanout # @see Channel#topic # @see Channel#headers # @see Queue # @see https://www.rabbitmq.com/resources/specs/amqp0-9-1.pdf AMQP 0.9.1 specification (Section 2.1.1) # @see https://www.rabbitmq.com/resources/specs/amqp0-9-1.pdf AMQP 0.9.1 specification (Section 2.1.5) # @see https://www.rabbitmq.com/resources/specs/amqp0-9-1.pdf AMQP 0.9.1 specification (Section 3.1.3) class Exchange # # Behaviours # include Entity extend ProtocolMethodHandlers # # API # DEFAULT_CONTENT_TYPE = "application/octet-stream".freeze BUILTIN_TYPES = [:fanout, :direct, :topic, :headers].freeze # The default exchange. Default exchange is a direct exchange that is predefined. # It cannot be removed. Every queue is bind to this (direct) exchange by default with # the following routing semantics: messages will be routed to the queue withe same # same name as message's routing key. In other words, if a message is published with # a routing key of "weather.usa.ca.sandiego" and there is a queue Q with this name, # that message will be routed to Q. # # @param [AMQP::Channel] channel Channel to use. If not given, new AMQP channel # will be opened on the default AMQP connection (accessible as AMQP.connection). # # @example Publishing a messages to the tasks queue # channel = AMQP::Channel.new(connection) # tasks_queue = channel.queue("tasks") # AMQP::Exchange.default(channel).publish("make clean", routing_key => "tasks") # # @see Exchange # @see https://www.rabbitmq.com/resources/specs/amqp0-9-1.pdf AMQP 0.9.1 specification (Section 2.1.2.4) # @note Do not confuse default exchange with amq.direct: amq.direct is a pre-defined direct # exchange that doesn't have any special routing semantics. # @return [Exchange] An instance that corresponds to the default exchange (of type direct). # @api public def self.default(channel = nil) self.new(channel || AMQP::Channel.new, :direct, AMQ::Protocol::EMPTY_STRING, :no_declare => true) end # @return [String] attr_reader :name # Type of this exchange (one of: :direct, :fanout, :topic, :headers). # @return [Symbol] attr_reader :type # @return [Symbol] # @api plugin attr_reader :status # Options hash this exchange instance was instantiated with # @return [Hash] attr_accessor :opts # @return [#call] A callback that is executed once declaration notification (exchange.declare-ok) # from the broker arrives. attr_accessor :on_declare # Channel this exchange belongs to. attr_reader :channel # @return [Hash] Additional arguments given on queue declaration. Typically used by AMQP extensions. attr_reader :arguments # @return [String] attr_reader :default_routing_key alias key default_routing_key # Compatibility alias for #on_declare. # # @api public # @deprecated # @return [#call] def callback @on_declare end # See {Exchange Exchange class documentation} for introduction, information about exchange types, # what uses cases they are good for and so on. # # h2. Predeclared exchanges # # If exchange name corresponds to one of those predeclared by AMQP 0.9.1 specification (empty string, amq.direct, amq.fanout, amq.topic, amq.match), # declaration command won't be sent to the broker (because the only possible reply from the broker is to reject it, predefined entities cannot be changed). # Callback, if any, will be executed immediately. # # # # @example Instantiating a fanout exchange using constructor # # AMQP.connect do |connection| # AMQP::Channel.new(connection) do |channel| # AMQP::Exchange.new(channel, :fanout, "search.index.updates") do |exchange, declare_ok| # # by now exchange is ready and waiting # end # end # end # # # @example Instantiating a direct exchange using {Channel#direct} # # AMQP.connect do |connection| # AMQP::Channel.new(connection) do |channel| # channel.direct("email.replies_listener") do |exchange, declare_ok| # # by now exchange is ready and waiting # end # end # end # # # @param [Channel] channel AMQP channel this exchange is associated with # @param [Symbol] type Exchange type # @param [String] name Exchange name # # # @option opts [Boolean] :passive (false) If set, the server will not create the exchange if it does not # already exist. The client can use this to check whether an exchange # exists without modifying the server state. # # @option opts [Boolean] :durable (false) If set when creating a new exchange, the exchange will be marked as # durable. Durable exchanges and their bindings are recreated upon a server # restart (information about them is persisted). Non-durable (transient) exchanges # do not survive if/when a server restarts (information about them is stored exclusively # in RAM). # # # @option opts [Boolean] :auto_delete (false) If set, the exchange is deleted when all queues have finished # using it. The server waits for a short period of time before # determining the exchange is unused to give time to the client code # to bind a queue to it. # # @option opts [Boolean] :internal (false) 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. *This is a RabbitMQ-specific # extension.* # # @option opts [Boolean] :nowait (true) 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. # # @option opts [Boolean] :no_declare (true) If set, exchange declaration command won't be sent to the broker. Allows to forcefully # avoid declaration. We recommend that only experienced developers consider this option. # # @option opts [String] :default_routing_key (nil) Default routing key that will be used by {Exchange#publish} when no routing key is not passed explicitly. # It is perfectly fine for applications to always specify routing key to {Exchange#publish}. # # @option opts [Hash] :arguments (nil) A hash of optional arguments with the declaration. Some brokers implement # AMQP extensions using x-prefixed declaration arguments. # # @yield [exchange, declare_ok] Yields successfully declared exchange instance and AMQP method (exchange.declare-ok) instance. The latter is optional. # @yieldparam [Exchange] exchange Exchange that is successfully declared and is ready to be used. # @yieldparam [AMQP::Protocol::Exchange::DeclareOk] declare_ok AMQP exchange.declare-ok) instance. # # @see Channel#default_exchange # @see Channel#direct # @see Channel#fanout # @see Channel#topic # @see Channel#headers # @see Queue # @see https://www.rabbitmq.com/resources/specs/amqp0-9-1.pdf AMQP 0.9.1 specification (Section 3.1.3) # # @return [Exchange] # @api public def initialize(channel, type, name, opts = {}, &block) @channel = channel @type = type @opts = self.class.add_default_options(type, name, opts, block) @default_routing_key = opts[:routing_key] || opts[:key] || AMQ::Protocol::EMPTY_STRING @name = name unless name.empty? @status = :unknown @default_publish_options = (opts.delete(:default_publish_options) || { :routing_key => @default_routing_key, :mandatory => false }).freeze @default_headers = (opts.delete(:default_headers) || { :content_type => DEFAULT_CONTENT_TYPE, :persistent => false, :priority => 0 }).freeze if !(BUILTIN_TYPES.include?(type.to_sym) || type.to_s =~ /^x-.+/i) raise UnknownExchangeTypeError.new(BUILTIN_TYPES, type) end @channel = channel @name = name @type = type # register pre-declared exchanges if @name == AMQ::Protocol::EMPTY_STRING || @name =~ /^amq\.(direct|fanout|topic|match|headers)/ @channel.register_exchange(self) end super(channel.connection) # The AMQP 0.8 specification (as well as 0.9.1) in 1.1.4.2 mentiones # that Exchange.Declare-Ok confirms the name of the exchange (because # of automatically­named), which is logical to interpret that this # functionality should be the same as for Queue (though it isn't # explicitely told in the specification). In fact, RabbitMQ (and # probably other implementations as well) doesn't support it and # there is a default exchange with an empty name (so-called default # or nameless exchange), so if we'd send Exchange.Declare(exchange=""), # then RabbitMQ interpret it as if we'd try to redefine this default # exchange so it'd produce an error. unless name == "amq.#{type}" or name.empty? or opts[:no_declare] @status = :opening unless @opts[:no_declare] @channel.once_open do if block shim = Proc.new do |exchange, declare_ok| case block.arity when 1 then block.call(exchange) else block.call(exchange, declare_ok) end end self.exchange_declare(@opts[:passive], @opts[:durable], @opts[:auto_delete], @opts[:internal], @opts[:nowait], @opts[:arguments], &shim) else self.exchange_declare(@opts[:passive], @opts[:durable], @opts[:auto_delete], @opts[:internal], @opts[:nowait], @opts[:arguments]) end end end else # Call the callback immediately, as given exchange is already # declared. @status = :opened block.call(self) if block end @on_declare = block end # @return [Channel] # @api public def channel @channel end # @return [Boolean] true if this exchange is of type `fanout` # @api public def fanout? @type == :fanout end # @return [Boolean] true if this exchange is of type `direct` # @api public def direct? @type == :direct end # @return [Boolean] true if this exchange is of type `topic` # @api public def topic? @type == :topic end # @return [Boolean] true if this exchange is of type `headers` # @api public def headers? @type == :headers end # @return [Boolean] true if this exchange is of a custom type (begins with x-) # @api public def custom_type? @type.to_s =~ /^x-.+/i end # custom_type? # @return [Boolean] true if this exchange is a pre-defined one (amq.direct, amq.fanout, amq.match and so on) def predefined? @name && ((@name == AMQ::Protocol::EMPTY_STRING) || !!(@name =~ /^amq\.(direct|fanout|topic|headers|match)/i)) end # predefined? # @return [Boolean] true if this exchange is an internal exchange def internal? @opts[:internal] end # Publishes message to the exchange. The message will be routed to queues by the exchange # and distributed to any active consumers. Routing logic is determined by exchange type and # configuration as well as message attributes (like :routing_key or message headers). # # Published data is opaque and not modified by Ruby amqp gem in any way. Serialization of data with JSON, Thrift, BSON # or similar libraries before publishing is very common. # # # h2. Data serialization # # Note that this method calls #to_s on payload argument value. Applications are encouraged of # data serialization before publishing (using JSON, Thrift, Protocol Buffers or other serialization library). # Note that because AMQP is a binary protocol, text formats like JSON largely lose their strong point of being easy # to inspect as data travels across network, so "BSON":http://bsonspec.org may be a good fit. # # # h2. Publishing and message persistence # # In cases when you application cannot afford to lose a message, AMQP 0.9.1 has several features to offer: # # * Persistent messages # * Messages acknowledgements # * Transactions # * (a RabbitMQ-specific extension) Publisher confirms # # This is a broad topic and we dedicate a separate guide, {file:docs/Durability.textile Durability and message persistence}, to it. # # # h2. Publishing callback and guarantees it DOES NOT offer # # Exact moment when message is published is not determined and depends on many factors, including machine's networking stack configuration, # so (optional) block this method takes is scheduled for next event loop tick, and data is staged for delivery for current event loop # tick. For most applications, this is good enough. The only way to guarantee a message was delivered in a distributed system is to # ask a peer to send you a message back. RabbitMQ # # @note Optional callback this method takes DOES NOT OFFER ANY GUARANTEES ABOUT DATA DELIVERY and must not be used as a "delivery callback". # The only way to guarantee delivery in distributed environment is to use an acknowledgement mechanism, such as AMQP transactions # or lightweight "publisher confirms" RabbitMQ extension supported by amqp gem. See {file:docs/Durability.textile Durability and message persistence} # and {file:docs/Exchanges.textile Working With Exchanges} guides for details. # # # h2. Event loop blocking # # When intermixing publishing of many messages with other workload that may take some time, even loop blocking may affect the performance. # There are several ways to avoid it: # # * Run EventMachine in a separate thread. # * Use EventMachine.next_tick. # * Use EventMachine.defer to offload operation to EventMachine thread pool. # # TBD: this subject is worth a separate guide # # # h2. Sending one-off messages # # If you need to send a one-off message and then stop the event loop, pass a block to {Exchange#publish} that will be executed # after message is pushed down the network stack, and use {AMQP::Session#disconnect} to properly tear down AMQP connection # (see example under Examples section below). # # @example Publishing a one-off message and properly closing AMQP connection then stopping the event loop: # exchange.publish(data) do # connection.disconnect { EventMachine.stop } # end # # # # @param [#to_s] payload Message payload (content). Note that this method calls #to_s on payload argument value. # You are encouraged to take care of data serialization before publishing (using JSON, Thrift, # Protocol Buffers or other serialization library). # # @option options [String] :routing_key (nil) Specifies message routing key. Routing key determines # what queues messages are delivered to (exact routing algorithms vary # between exchange types). # # @option options [Boolean] :mandatory (false) This flag tells the server how to react if the message cannot be # routed to a queue. If message is mandatory, the server will return # unroutable message back to the client with basic.return AMQPmethod. # If message is not mandatory, the server silently drops the message. # # @option options [Boolean] :persistent (false) When true, this message will be persisted to disk and remain in the queue until # it is consumed. When false, the message is only kept in a transient store # and will lost in case of server restart. # When performance and latency are more important than durability, set :persistent => false. # If durability is more important, set :persistent => true. # # @option options [String] :content_type (application/octet-stream) Content-type of message payload. # # # @example Publishing without routing key # exchange = channel.fanout('search.indexer') # # fanout exchanges deliver messages to bound queues unconditionally, # # so routing key is unnecessary here # exchange.publish("some data") # # @example Publishing with a routing key # exchange = channel.direct('search.indexer') # exchange.publish("some data", :routing_key => "search.index.updates") # # @return [Exchange] self # # @note Please make sure you read {file:docs/Durability.textile Durability an message persistence} guide that covers exchanges durability vs. messages # persistence. # @api public def publish(payload, options = {}, &block) opts = @default_publish_options.merge(options) @channel.once_open do properties = @default_headers.merge(options) properties[:delivery_mode] = properties.delete(:persistent) ? 2 : 1 basic_publish(payload.to_s, opts[:key] || opts[:routing_key] || @default_routing_key, properties, opts[:mandatory]) # don't pass block to AMQP::Exchange#publish because it will be executed # immediately and we want to do it later. See ruby-amqp/amqp/#67 MK. EventMachine.next_tick(&block) if block end self end # This method deletes an exchange. When an exchange is deleted all queue bindings on the exchange are deleted, too. # Further attempts to publish messages to a deleted exchange will result in a channel-level exception. # # @example Deleting an exchange # # exchange = AMQP::Channel.direct("search.indexing") # exchange.delete # # @option opts [Boolean] :nowait (false) 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. # # @option opts [Boolean] :if_unused (false) 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. # # @return [NilClass] nil # @api public def delete(opts = {}, &block) @channel.once_open do exchange_delete(opts.fetch(:if_unused, false), opts.fetch(:nowait, false), &block) end # backwards compatibility nil end # @return [Boolean] true if this exchange is durable # @note Please make sure you read {Exchange Exchange class} documentation section on exchanges durability vs. messages # persistence. # @api public def durable? !!@opts[:durable] end # durable? # @return [Boolean] true if this exchange is transient (non-durable) # @note Please make sure you read {Exchange Exchange class} documentation section on exchanges durability vs. messages # persistence. # @api public def transient? !self.durable? end # transient? alias temporary? transient? # @return [Boolean] true if this exchange is automatically deleted when it is no longer used # @api public def auto_deleted? !!@opts[:auto_delete] end # auto_deleted? alias auto_deletable? auto_deleted? # Resets queue state. Useful for error handling. # @api plugin def reset initialize(@channel, @type, @name, @opts) end # @group Declaration # @api public def exchange_declare(passive = false, durable = false, auto_delete = false, internal = false, nowait = false, arguments = nil, &block) # for re-declaration @passive = passive @durable = durable @auto_delete = auto_delete @arguments = arguments @internal = internal @connection.send_frame(AMQ::Protocol::Exchange::Declare.encode(@channel.id, @name, @type.to_s, passive, durable, auto_delete, internal, nowait, arguments)) unless nowait self.define_callback(:declare, &block) @channel.exchanges_awaiting_declare_ok.push(self) end self end # @api public def redeclare(&block) nowait = block.nil? @connection.send_frame(AMQ::Protocol::Exchange::Declare.encode(@channel.id, @name, @type.to_s, @passive, @durable, @auto_delete, @internal, nowait, @arguments)) unless nowait self.define_callback(:declare, &block) @channel.exchanges_awaiting_declare_ok.push(self) end self end # redeclare(&block) # @endgroup # @api public def exchange_delete(if_unused = false, nowait = false, &block) @connection.send_frame(AMQ::Protocol::Exchange::Delete.encode(@channel.id, @name, if_unused, nowait)) unless nowait self.define_callback(:delete, &block) # TODO: delete itself from exchanges cache @channel.exchanges_awaiting_delete_ok.push(self) end self end # delete(if_unused = false, nowait = false) # @group Exchange to Exchange Bindings # This method binds a source exchange to a destination exchange. Messages # sent to the source exchange are forwarded to the destination exchange, provided # the message routing key matches the routing key specified when binding the # exchanges. # # A valid exchange name (or reference) must be passed as the first # parameter. # @example Binding two source exchanges to a destination exchange # # ch = AMQP::Channel.new(connection) # source1 = ch.fanout('backlog-events-datacenter-1') # source2 = ch.fanout('backlog-events-datacenter-2') # destination = ch.fanout('baklog-events') # destination.bind(source1).bind(source2) # # # @param [Exchange] Exchange to bind to. May also be a string or any object that responds to #name. # # @option opts [String] :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. # # @option opts [Hash] :arguments (nil) A hash of optional arguments with the declaration. Headers exchange type uses these metadata # attributes for routing matching. # In addition, brokers may implement AMQP extensions using x-prefixed declaration arguments. # # @option opts [Boolean] :nowait (true) 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. # @return [Exchange] Self # # # @yield [] Since exchange.bind-ok carries no attributes, no parameters are yielded to the block. # # @api public def bind(source, opts = {}, &block) source = source.name if source.respond_to?(:name) routing_key = opts[:key] || opts[:routing_key] || AMQ::Protocol::EMPTY_STRING arguments = opts[:arguments] || {} nowait = opts[:nowait] || block.nil? @channel.once_open do @connection.send_frame(AMQ::Protocol::Exchange::Bind.encode(@channel.id, @name, source, routing_key, nowait, arguments)) unless nowait self.define_callback(:bind, &block) @channel.exchanges_awaiting_bind_ok.push(self) end end self end # This method unbinds a source exchange from a previously bound destination exchange. # # A valid exchange name (or reference) must be passed as the first # parameter. # @example Binding and unbinding two exchanges # # ch = AMQP::Channel.new(connection) # source = ch.fanout('backlog-events') # destination = ch.fanout('backlog-events-copy') # destination.bind(source) # ... # destination.unbind(source) # # # @param [Exchange] Exchange to bind to. May also be a string or any object that responds to #name. # # @option opts [String] :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. # # @option opts [Hash] :arguments (nil) A hash of optional arguments with the declaration. Headers exchange type uses these metadata # attributes for routing matching. # In addition, brokers may implement AMQP extensions using x-prefixed declaration arguments. # # @option opts [Boolean] :nowait (true) 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. # @return [Exchange] Self # # # @yield [] Since exchange.unbind-ok carries no attributes, no parameters are yielded to the block. # # @api public def unbind(source, opts = {}, &block) source = source.name if source.respond_to?(:name) routing_key = opts[:key] || opts[:routing_key] || AMQ::Protocol::EMPTY_STRING arguments = opts[:arguments] || {} nowait = opts[:nowait] || block.nil? @channel.once_open do @connection.send_frame(AMQ::Protocol::Exchange::Unbind.encode(@channel.id, @name, source, routing_key, nowait, arguments)) unless nowait self.define_callback(:unbind, &block) @channel.exchanges_awaiting_unbind_ok.push(self) end end self end # @endgroup # @group Publishing Messages # @api public def basic_publish(payload, routing_key = AMQ::Protocol::EMPTY_STRING, user_headers = {}, mandatory = false) headers = { :priority => 0, :delivery_mode => 2, :content_type => "application/octet-stream" }.merge(user_headers) @connection.send_frameset(AMQ::Protocol::Basic::Publish.encode(@channel.id, payload, headers, @name, routing_key, mandatory, false, @connection.frame_max), @channel) # publisher confirms support. MK. @channel.exec_callback(:after_publish) self end # @api public def on_return(&block) self.redefine_callback(:return, &block) self end # on_return(&block) # @endgroup # @group Error Handling and Recovery # Defines a callback that will be executed after TCP connection is interrupted (typically because of a network failure). # Only one callback can be defined (the one defined last replaces previously added ones). # # @api public def on_connection_interruption(&block) self.redefine_callback(:after_connection_interruption, &block) end # on_connection_interruption(&block) alias after_connection_interruption on_connection_interruption # @private def handle_connection_interruption(method = nil) self.exec_callback_yielding_self(:after_connection_interruption) end # handle_connection_interruption # Defines a callback that will be executed after TCP connection is recovered after a network failure # but before AMQP connection is re-opened. # Only one callback can be defined (the one defined last replaces previously added ones). # # @api public def before_recovery(&block) self.redefine_callback(:before_recovery, &block) end # before_recovery(&block) # @private def run_before_recovery_callbacks self.exec_callback_yielding_self(:before_recovery) end # Defines a callback that will be executed when AMQP connection is recovered after a network failure.. # Only one callback can be defined (the one defined last replaces previously added ones). # # @api public def on_recovery(&block) self.redefine_callback(:after_recovery, &block) end # on_recovery(&block) alias after_recovery on_recovery # @private def run_after_recovery_callbacks self.exec_callback_yielding_self(:after_recovery) end # Called by associated connection object when AMQP connection has been re-established # (for example, after a network failure). # # @api plugin def auto_recover self.redeclare unless predefined? end # auto_recover # @endgroup # # Implementation # def handle_declare_ok(method) @channel.register_exchange(self) self.exec_callback_once_yielding_self(:declare, method) end def handle_bind_ok(method) self.exec_callback_once(:bind, method) end def handle_unbind_ok(method) self.exec_callback_once(:unbind, method) end def handle_delete_ok(method) self.exec_callback_once(:delete, method) end self.handle(AMQ::Protocol::Exchange::DeclareOk) do |connection, frame| method = frame.decode_payload channel = connection.channels[frame.channel] exchange = channel.exchanges_awaiting_declare_ok.shift exchange.handle_declare_ok(method) end # handle self.handle(AMQ::Protocol::Exchange::BindOk) do |connection, frame| method = frame.decode_payload channel = connection.channels[frame.channel] exchange = channel.exchanges_awaiting_bind_ok.shift exchange.handle_bind_ok(method) end # handle self.handle(AMQ::Protocol::Exchange::UnbindOk) do |connection, frame| method = frame.decode_payload channel = connection.channels[frame.channel] exchange = channel.exchanges_awaiting_unbind_ok.shift exchange.handle_unbind_ok(method) end # handle self.handle(AMQ::Protocol::Exchange::DeleteOk) do |connection, frame| channel = connection.channels[frame.channel] exchange = channel.exchanges_awaiting_delete_ok.shift exchange.handle_delete_ok(frame.decode_payload) end # handle self.handle(AMQ::Protocol::Basic::Return) do |connection, frame, content_frames| channel = connection.channels[frame.channel] method = frame.decode_payload exchange = channel.find_exchange(method.exchange) header = content_frames.shift body = content_frames.map { |frame| frame.payload }.join exchange.exec_callback(:return, method, header, body) end protected # @private def self.add_default_options(type, name, opts, block) { :exchange => name, :type => type, :nowait => block.nil?, :internal => false }.merge(opts) end end # Exchange end # AMQP amqp-1.8.0/lib/amqp/framing/0000755000004100000410000000000013321132064015630 5ustar www-datawww-dataamqp-1.8.0/lib/amqp/framing/string/0000755000004100000410000000000013321132064017136 5ustar www-datawww-dataamqp-1.8.0/lib/amqp/framing/string/frame.rb0000644000004100000410000000216213321132064020556 0ustar www-datawww-data# encoding: utf-8 require "amqp/exceptions" module AMQP module Framing module String class Frame < AMQ::Protocol::Frame ENCODINGS_SUPPORTED = defined? Encoding HEADER_SLICE = (0..6).freeze DATA_SLICE = (7..-1).freeze PAYLOAD_SLICE = (0..-2).freeze def self.decode(string) header = string[HEADER_SLICE] type, channel, size = self.decode_header(header) data = string[DATA_SLICE] payload = data[PAYLOAD_SLICE] frame_end = data[-1, 1] frame_end.force_encoding(AMQ::Protocol::Frame::FINAL_OCTET.encoding) if ENCODINGS_SUPPORTED # 1) the size is miscalculated if payload.bytesize != size raise BadLengthError.new(size, payload.bytesize) end # 2) the size is OK, but the string doesn't end with FINAL_OCTET raise NoFinalOctetError.new if frame_end != AMQ::Protocol::Frame::FINAL_OCTET self.new(type, payload, channel) end # self.from end # Frame end # String end # Framing end # AMQP amqp-1.8.0/lib/amqp/entity.rb0000644000004100000410000000332313321132064016047 0ustar www-datawww-datarequire "amqp/openable" require "amqp/callbacks" module AMQP module RegisterEntityMixin # @example Registering Channel implementation # Adapter.register_entity(:channel, Channel) # # ... so then I can do: # channel = client.channel(1) # # instead of: # channel = Channel.new(client, 1) def register_entity(name, klass) define_method(name) do |*args, &block| klass.new(self, *args, &block) end # define_method end # register_entity end # RegisterEntityMixin module ProtocolMethodHandlers def handle(klass, &block) HandlersRegistry.register(klass, &block) end def handlers HandlersRegistry.handlers end end # ProtocolMethodHandlers # AMQ entities, as implemented by AMQP, have callbacks and can run them # when necessary. # # @note Exchanges and queues implementation is based on this class. # # @abstract module Entity # # Behaviors # include Openable include Callbacks # # API # # @return [Array<#call>] attr_reader :callbacks def initialize(connection) @connection = connection # Be careful with default values for #ruby hashes: h = Hash.new(Array.new); h[:key] ||= 1 # won't assign anything to :key. MK. @callbacks = Hash.new end # initialize end # Entity # Common behavior of AMQ entities that can be either client or server-named, for example, exchanges and queues. module ServerNamedEntity # @return [Boolean] true if this entity is anonymous (server-named) def server_named? @server_named || @name.nil? || @name.empty? end # backwards compabitility. MK. alias anonymous? server_named? end # ServerNamedEntity end amqp-1.8.0/lib/amqp/bit_set.rb0000644000004100000410000000032213321132064016160 0ustar www-datawww-data# encoding: utf-8 require "amq/bit_set" module AMQP # A forward reference for AMQP::BitSet that was extracted to amq-protocol # to make it possible to reuse it in Bunny. BitSet = AMQ::BitSet end # AMQP amqp-1.8.0/lib/amqp/int_allocator.rb0000644000004100000410000000035213321132064017364 0ustar www-datawww-data# encoding: utf-8 require "amq/int_allocator" module AMQP # A forward reference for AMQP::IntAllocator that was extracted to amq-protocol # to make it possible to reuse it in Bunny. IntAllocator = AMQ::IntAllocator end # AMQP amqp-1.8.0/lib/amqp/compatibility/0000755000004100000410000000000013321132064017056 5ustar www-datawww-dataamqp-1.8.0/lib/amqp/compatibility/ruby187_patchlevel_check.rb0000644000004100000410000000204413321132064024170 0ustar www-datawww-data# encoding: utf-8 if RUBY_VERSION =~ /^1.8.7/ require "rbconfig" conf = RbConfig::CONFIG # looks like PATCHLEVEL was added after 1.8.7-p249 release :( MK. if (conf["PATCHLEVEL"] && conf["PATCHLEVEL"] == "249") || (conf["libdir"] =~ /1.8.7-p249/) raise <<-MESSAGE IMPORTANT: amqp gem 0.8.0+ does not support Ruby 1.8.7-p249 (this exact patch level. p174, p334 and later patch levels are supported!) because of a nasty Ruby bug (http://bit.ly/iONBmH). Refer to http://groups.google.com/group/ruby-amqp/browse_thread/thread/37d6dcc1aea1c102 for more information, especially if you want to verify whether a particular Ruby patch level or version or build suffers from this issue. To reiterate: 1.8.7-p174, p334 and later patch levels are supported, see http://travis-ci.org/ruby-amqp/amqp. The fix was committed to MRI in December 2009. It's a good idea to upgrade, although downgrading to p174 is an option, too. To learn more (including the 0.8.x migration guide) at http://bit.ly/rubyamqp and https://github.com/ruby-amqp. MESSAGE end end amqp-1.8.0/lib/amqp/channel_id_allocator.rb0000644000004100000410000000302713321132064020660 0ustar www-datawww-datamodule AMQP module ChannelIdAllocator MAX_CHANNELS_PER_CONNECTION = (2**16) - 1 # Resets channel allocator. This method is thread safe. # @api public # @see Channel.next_channel_id # @see Channel.release_channel_id def reset_channel_id_allocator channel_id_mutex.synchronize do int_allocator.reset end end # Releases previously allocated channel id. This method is thread safe. # # @param [Fixnum] Channel id to release # @api public # @see Channel.next_channel_id # @see Channel.reset_channel_id_allocator def release_channel_id(i) channel_id_mutex.synchronize do int_allocator.release(i) end end # Returns next available channel id. This method is thread safe. # # @return [Fixnum] # @api public # @see Channel.release_channel_id # @see Channel.reset_channel_id_allocator def next_channel_id channel_id_mutex.synchronize do result = int_allocator.allocate raise "No further channels available. Please open a new connection." if result < 0 result end end private # @private # @api private def channel_id_mutex @channel_id_mutex ||= Mutex.new end # @private def int_allocator # TODO: ideally, this should be in agreement with agreed max number of channels of the connection, # but it is possible that value either not yet available. MK. @int_allocator ||= IntAllocator.new(1, MAX_CHANNELS_PER_CONNECTION) end end end amqp-1.8.0/lib/amqp/integration/0000755000004100000410000000000013321132064016530 5ustar www-datawww-dataamqp-1.8.0/lib/amqp/integration/rails.rb0000644000004100000410000000114413321132064020167 0ustar www-datawww-datarequire "yaml" module AMQP module Integration class Rails def self.start(options_or_uri = {}, &block) yaml = YAML.load_file(File.join(::Rails.root, "config", "amqp.yml")) settings = yaml.fetch(::Rails.env, Hash.new).symbolize_keys arg = if options_or_uri.is_a?(Hash) settings.merge(options_or_uri)[:uri] else settings[:uri] || options_or_uri end EventMachine.next_tick do AMQP.start(arg, &block) end end end # Rails end # Integration end # AMQP amqp-1.8.0/lib/amqp/auth_mechanism_adapter/0000755000004100000410000000000013321132064020672 5ustar www-datawww-dataamqp-1.8.0/lib/amqp/auth_mechanism_adapter/external.rb0000644000004100000410000000161113321132064023040 0ustar www-datawww-data# encoding: utf-8 module AMQP # Manages the encoding of credentials for the EXTERNAL authentication # mechanism. class AuthMechanismAdapter::External < AuthMechanismAdapter auth_mechanism "EXTERNAL" # Encodes a username and password for the EXTERNAL mechanism. Since # authentication is handled by an encapsulating protocol like SSL or # UNIX domain sockets, EXTERNAL doesn't pass along any username or # password information at all and this method always returns the # empty string. # # @param [String] username The username to encode. This parameter is # ignored. # @param [String] password The password to encode. This parameter is # ignored. # @return [String] The username and password, encoded for the # EXTERNAL mechanism. This is always the empty string. def encode_credentials(username, password) "" end end end amqp-1.8.0/lib/amqp/auth_mechanism_adapter/plain.rb0000644000004100000410000000133413321132064022323 0ustar www-datawww-data# encoding: utf-8 module AMQP # Manages the encoding of credentials for the PLAIN authentication # mechanism. class AuthMechanismAdapter::Plain < AuthMechanismAdapter auth_mechanism "PLAIN" # Encodes credentials for the given username and password. This # involves sending the password across the wire in plaintext, so # PLAIN authentication should only be used over a secure transport # layer. # # @param [String] username The username to encode. # @param [String] password The password to encode. # @return [String] The username and password, encoded for the PLAIN # mechanism. def encode_credentials(username, password) "\0#{username}\0#{password}" end end end amqp-1.8.0/lib/amqp/extensions/0000755000004100000410000000000013321132064016404 5ustar www-datawww-dataamqp-1.8.0/lib/amqp/extensions/rabbitmq.rb0000644000004100000410000000004513321132064020531 0ustar www-datawww-data# encoding: utf-8 # No-op as of 1.1 amqp-1.8.0/lib/amqp/callbacks.rb0000644000004100000410000000307113321132064016452 0ustar www-datawww-datamodule AMQP module Callbacks def redefine_callback(event, callable = nil, &block) f = (callable || block) # yes, re-assign! @callbacks[event] = [f] self end def define_callback(event, callable = nil, &block) f = (callable || block) @callbacks[event] ||= [] @callbacks[event] << f if f self end # define_callback(event, &block) alias append_callback define_callback def prepend_callback(event, &block) @callbacks[event] ||= [] @callbacks[event].unshift(block) self end # prepend_callback(event, &block) def clear_callbacks(event) @callbacks[event].clear if @callbacks[event] end # clear_callbacks(event) def exec_callback(name, *args, &block) list = Array(@callbacks[name]) if list.any? list.each { |c| c.call(*args, &block) } end end def exec_callback_once(name, *args, &block) list = (@callbacks.delete(name) || Array.new) if list.any? list.each { |c| c.call(*args, &block) } end end def exec_callback_yielding_self(name, *args, &block) list = Array(@callbacks[name]) if list.any? list.each { |c| c.call(self, *args, &block) } end end def exec_callback_once_yielding_self(name, *args, &block) list = (@callbacks.delete(name) || Array.new) if list.any? list.each { |c| c.call(self, *args, &block) } end end def has_callback?(name) @callbacks[name] && !@callbacks[name].empty? end # has_callback? end # Callbacks end amqp-1.8.0/lib/amq/0000755000004100000410000000000013321132064014025 5ustar www-datawww-dataamqp-1.8.0/lib/amq/protocol/0000755000004100000410000000000013321132064015666 5ustar www-datawww-dataamqp-1.8.0/lib/amq/protocol/get_response.rb0000644000004100000410000000211513321132064020707 0ustar www-datawww-data# encoding: utf-8 # Purpose of this class is to simplify work with GetOk and GetEmpty. module AMQ module Protocol class GetResponse attr_reader :method def initialize(method) @method = method end def empty? @method.is_a?(::AMQ::Protocol::Basic::GetEmpty) end # GetOk attributes def delivery_tag if @method.respond_to?(:delivery_tag) @method.delivery_tag end end def redelivered if @method.respond_to?(:redelivered) @method.redelivered end end def exchange if @method.respond_to?(:exchange) @method.exchange end end def routing_key if @method.respond_to?(:routing_key) @method.routing_key end end def message_count if @method.respond_to?(:message_count) @method.message_count end end # GetEmpty attributes def cluster_id if @method.respond_to?(:cluster_id) @method.cluster_id end end end end end amqp-1.8.0/lib/amqp.rb0000644000004100000410000002104713321132064014536 0ustar www-datawww-data# encoding: utf-8 require "amq/protocol" require "amqp/version" if RUBY_VERSION =~ /^1.8.7/ require "amqp/compatibility/ruby187_patchlevel_check" end require "amqp/handlers_registry" require "amqp/callbacks" require "amqp/entity" require "amqp/exceptions" require "amqp/settings" require "amqp/deferrable" require "amqp/session" require "amqp/exchange" require "amqp/queue" require "amqp/channel" require "amqp/header" # Top-level namespace of amqp gem. Please refer to "See also" section below. # # @see AMQP.connect # @see AMQP.start # @see AMQP::Channel # @see AMQP::Exchange # @see AMQP::Queue module AMQP # Starts EventMachine event loop unless it is already running and connects # to AMQP broker using {AMQP.connect}. It is generally a good idea to # start EventMachine event loop in a separate thread and use {AMQP.connect} # (for Web applications that do not use Thin or Goliath, it is the only option). # # See {AMQP.connect} for information about arguments this method takes and # information about relevant topics such as authentication failure handling. # # @example Using AMQP.start to connect to AMQP broker, EventMachine loop isn't yet running # AMQP.start do |connection| # # default is to connect to localhost:5672, to root ("/") vhost as guest/guest # # # this block never exits unless either AMQP.stop or EM.stop # # is called. # # AMQP::Channel(connection) do |channel| # channel.queue("", :auto_delete => true).bind(channel.fanout("amq.fanout")).subscribe do |headers, payload| # # handle deliveries here # end # end # end # # @api public def self.start(connection_options_or_string = {}, other_options = {}, &block) EM.run do if !@connection || @connection.closed? || @connection.closing? @connection = connect(connection_options_or_string, other_options, &block) end @channel = Channel.new(@connection) @connection end end # Alias for {AMQP.start} # @api public def self.run(*args, &block) self.start(*args, &block) end # Properly closes default AMQP connection and then underlying TCP connection. # Pass it a block if you want a piece of code to be run once default connection # is successfully closed. # # @note If default connection was never established or is in the closing state already, # this method has no effect. # @api public def self.stop(reply_code = 200, reply_text = "Goodbye", &block) return if @connection.nil? || self.closing? EM.next_tick do if AMQP.channel and AMQP.channel.open? and AMQP.channel.connection.open? AMQP.channel.close end AMQP.channel = nil shim = Proc.new { block.call AMQP.connection = nil } @connection.disconnect(reply_code, reply_text, &shim) end end # Indicates that default connection is closing. # # @return [Boolean] # @api public def self.closing? @connection.closing? end # @return [Boolean] Current global logging value # @api public def self.logging self.settings[:logging] end # @return [Boolean] Sets current global logging value # @api public def self.logging=(value) self.settings[:logging] = !!value end # Default connection. When you do not pass connection instance to methods like # {Channel#initialize}, AMQP gem will use this default connection. # # @api public def self.connection @connection end # "Default channel". A placeholder for apps that only want to use one channel. This channel is not global, *not* used # under the hood by methods like {AMQP::Exchange#initialize} and only shared by exchanges/queues you decide on. # To reiterate: this is only a conventience accessor, since many apps (especially Web apps) can get by with just one # connection and one channel. # # @api public def self.channel @channel end # A placeholder for applications that only need one channel. If you use {AMQP.start} to set up default connection, # {AMQP.channel} is open on that connection, but can be replaced by your application. # # # @see AMQP.channel # @api public def self.channel=(value) @channel = value end # Sets global connection object. # @api public def self.connection=(value) @connection = value end # Alias for {AMQP.connection} # @deprecated # @api public def self.conn warn "AMQP.conn will be removed in 1.0. Please use AMQP.connection." @connection end # Alias for {AMQP.connection=} # @deprecated # @api public def self.conn=(value) warn "AMQP.conn= will be removed in 1.0. Please use AMQP.connection=(connection)." self.connection = value end # Connects to AMQP broker and yields connection object to the block as soon # as connection is considered open. # # # @example Using AMQP.connect with default connection settings # # AMQP.connect do |connection| # AMQP::Channel.new(connection) do |channel| # # channel is ready: set up your messaging flow by creating exchanges, # # queues, binding them together and so on. # end # end # # @example Using AMQP.connect to connect to a public RabbitMQ instance with connection settings given as a hash # # AMQP.connect(:host => "dev.rabbitmq.com", :username => "guest", :password => "guest") do |connection| # AMQP::Channel.new(connection) do |channel| # # ... # end # end # # # @example Using AMQP.connect to connect to a public RabbitMQ instance with connection settings given as a URI # # AMQP.connect "amqp://guest:guest@dev.rabbitmq.com:5672", :on_possible_authentication_failure => Proc.new { puts("Looks like authentication has failed") } do |connection| # AMQP::Channel.new(connection) do |channel| # # ... # end # end # # # @overload connect(connection_string, options = {}) # Used to pass connection parameters as a connection string # @param [String] :connection_string AMQP connection URI, à la JDBC connection string. For example: amqp://bus.megacorp.internal:5877/qa # # # @overload connect(connection_options) # Used to pass connection options as a Hash. # @param [Hash] :connection_options AMQP connection options (:host, :port, :username, :vhost, :password) # # @option connection_options_or_string [String] :host ("localhost") Host to connect to. # @option connection_options_or_string [Integer] :port (5672) Port to connect to. # @option connection_options_or_string [String] :vhost ("/") Virtual host to connect to. # @option connection_options_or_string [String] :username ("guest") Username to use. Also can be specified as :user. # @option connection_options_or_string [String] :password ("guest") Password to use. Also can be specified as :pass. # @option connection_options_or_string [Hash] :ssl TLS (SSL) parameters to use. # @option connection_options_or_string [Fixnum] :heartbeat (0) Connection heartbeat, in seconds. 0 means no heartbeat. Can also be configured server-side starting with RabbitMQ 3.0. # @option connection_options_or_string [#call] :on_tcp_connection_failure A callable object that will be run if connection to server fails # @option connection_options_or_string [#call] :on_possible_authentication_failure A callable object that will be run if authentication fails (see Authentication failure section) # # # h2. Handling authentication failures # # AMQP 0.9.1 specification dictates that broker closes TCP connection when it detects that authentication # has failed. However, broker does exactly the same thing when other connection-level exception occurs # so there is no way to guarantee that connection was closed because of authentication failure. # # Because of that, AMQP gem follows Java client example and hints at _possibility_ of authentication failure. # To handle it, pass a callable object (a proc, a lambda, an instance of a class that responds to #call) # with :on_possible_authentication_failure option. # # @note This method assumes that EventMachine even loop is already running. If it is not the case or you are not sure, we recommend you use {AMQP.start} instead. # It takes exactly the same parameters. # @return [AMQP::Session] # @api public def self.connect(connection_options_or_string = ENV['RABBITMQ_URL'], other_options = {}, &block) AMQP::Session.connect(connection_options_or_string, other_options, &block) end # @return [Hash] Default AMQP connection settings. This hash may be modified. # @api public def self.settings @settings ||= AMQ::Settings.default.merge(logging: false) end end # AMQP amqp-1.8.0/.yardopts0000644000004100000410000000024713321132064014352 0ustar www-datawww-data--no-private --protected --markup="textile" lib/**/*.rb --main README.md --asset docs:docs --asset examples:examples --hide-tag todo - LICENSE CHANGELOG docs/*.textileamqp-1.8.0/repl0000755000004100000410000000005213321132064013366 0ustar www-datawww-data#!/bin/sh bundle exec irb -Ilib -r'amqp' amqp-1.8.0/ChangeLog.md0000644000004100000410000001202613321132064014653 0ustar www-datawww-data## Changes Between 1.6.0 and 1.7.0 (Feb 2nd, 2017) ### Clear Framesets on Exception Unprocessed frames received on a connection are now correctly cleared when an exception occurs. Contributed by Michael Lutsiuk. GitHub issue: [#218](https://github.com/ruby-amqp/amqp/issues/218) ### amq-protocol Update Minimum `amq-protocol` version is now `2.1.0`. ## Changes Between 1.5.0 and 1.6.0 (Apr 4th, 2016) ### amq-protocol Update Minimum `amq-protocol` version is now `2.0.1`. ### Provide More Details in TCP Connection Failure Exception Contributed by Neil Hooey. GH issue: [#222](https://github.com/ruby-amqp/amqp/issues/222). ### Ensures frameset is cleared after an unhandled exception Ensures frameset is cleared after an unhandled exception. This avoids confusing exceptions such as ``` undefined method `method_class' for # ``` Contributed by Michael Lutsiuk. GH issue: [#218](https://github.com/ruby-amqp/amqp/issues/218) ## Changes Between 1.4.x and 1.5.0 ### Only Await basic.consume-ok If nowait is false Contributed by Rian McGuire. ### Server-Named Queue Recovery Fix Server-named queues are now correctly recovered again. Contributed by Jack C Hong. ## Changes Between 1.3.x and 1.4.0 ### connection.blocked Support [connection.blocked](https://www.rabbitmq.com/connection-blocked.html) notifications are now correctly supported by the library: ``` ruby EventMachine.run do connection = AMQP.connect(:host => '127.0.0.1') connection.on_blocked do |conn, conn_blocked| puts "Connection blocked, reason: #{conn_blocked.reason}" end connection.on_unblocked do |conn, _| puts "Connection unblocked" end end ``` ## Changes Between 1.2.x and 1.3.0 ### Exchange-to-Exchange Bindings Support amqp gem now supports [Exchange-to-Exchange Bindings](http://www.rabbitmq.com/e2e.html), a RabbitMQ extension. `AMQP::Exchange#bind` and `AMQP::Exchange#unbind` work very much like `AMQP::Queue#bind` and `AMQP::Queue#unbind`, with the argument exchange being the source one. Contributed by Stefan Kaes. ### Internal Exchange Declaration amqp gem now supports declaration of internal exchanges (used via exchange-to-exchange bindings, cannot be published to by clients). To declare an exchange as internal, add `:internal => true` to declaration options. Contributed by Stefan Kaes. ### Initial Connection Failures Retries Set connection status to closed on connection failure, which means connection retries succeed. Contributed by Marius Hanne. ## Changes Between 1.1.0 and 1.2.0 ### [Authentication Failure Notification](http://www.rabbitmq.com/auth-notification.html) Support amqp gem now supports [Authentication Failure Notification](http://www.rabbitmq.com/auth-notification.html). Public API for authentication failure handling hasn't changed. This extension is available in RabbitMQ 3.2+. ## basic.qos Recovery Fix `basic.qos` setting will now be recovered first thing after channel recovery, to the most recent value passed via `:prefetch` channel constructor option or `AMQP::Channel#prefetch`. ### amq-protocol Update Minimum `amq-protocol` version is now `1.9.2`. ### Automatic Recovery Fix Automatic connection recovery now correctly recovers bindings again. Contributed by Devin Christensen. ### 65535 Channels Per Connection amqp gem now allows for 65535 channels per connection and not Ruby process. Contributed by Neo (http://neo.com) developers. ### channel.close is Delayed Until After Channel is Open This eliminates a race condition in some codebases that use very short lived channels. ### ConnectionClosedError is Back `ConnectionClosedError` from `amq-client` is now defined again. ### Fixed Exceptions in AMQP::Exchange#handle_declare_ok `AMQP::Exchange#handle_declare_ok` no longer raises an exception about undefined methods `#anonymous?` and `#exchange`. ## Changes Between 1.0.0 and 1.1.0 ### amq-protocol Update Minimum `amq-protocol` version is now `1.8.0` which includes a bug fix for messages exactly 128 Kb in size. ### AMQ::Client is Removed `amq-client` has been incorporated into amqp gem. `AMQ::Client` and related modules are no longer available. ### AMQP::Channel#confirm_select is Now Delayed `AMQP::Channel#confirm_select` is now delayed until after the channel is opened, making it possible to use it with the pseudo-synchronous code style. ### RabbitMQ Extensions are Now in Core amqp gem has been targeting RabbitMQ exclusively for a while now. RabbitMQ extensions are now loaded by default and will be even more tightly integrated in the future. ### AMQP::Channel.default is Removed `AMQP::Channel.default` and method_missing-based operations on the default channel has been removed. They've been deprecated since 0.6. ### AMQP::Channel#rpc is Removed `AMQP::RPC`-related code has been removed. It has been deprecated since 0.7. ### AMQP::Channel.on_error is Removed Long time deprecated `AMQP::Channel.on_error` is removed. ## Version 1.0.0 ### Deprecated APIs are Being Removed Most of public API bits deprecated in 0.8.0 are COMPLETELY REMOVED. amqp-1.8.0/Gemfile0000644000004100000410000000221713321132064013776 0ustar www-datawww-data# encoding: utf-8 source "https://rubygems.org" # Use local clones if possible. # If you want to use your local copy, just symlink it to vendor. def custom_gem(name, options = Hash.new) local_path = File.expand_path("../vendor/#{name}", __FILE__) if File.exist?(local_path) puts "Using #{name} from #{local_path}..." gem name, options.merge(:path => local_path).delete_if { |key, _| [:git, :branch].include?(key) } else gem name, options end end custom_gem "eventmachine", "~> 1.2.1" custom_gem "amq-protocol", :git => "https://github.com/ruby-amqp/amq-protocol.git", :branch => "master" group :development do gem "yard", ">= 0.7.2" # yard tags this buddy along gem "RedCloth", :platform => :mri platform :ruby do gem "rdiscount" # To test event loop helper and various Rack apps gem "thin" gem "unicorn" end end group :test do gem "rspec", "~> 2.14.1" gem "rake", "~> 11.3.0" custom_gem "evented-spec", :git => "https://github.com/ruby-amqp/evented-spec.git", :branch => "master" gem "effin_utf8" gem "multi_json" gem "json", :platform => :jruby gem "yajl-ruby", :platform => :ruby_18 end amqp-1.8.0/amqp.gemspec0000755000004100000410000000171013321132064015006 0ustar www-datawww-data#!/usr/bin/env gem build # encoding: utf-8 require "base64" require File.expand_path("../lib/amqp/version", __FILE__) Gem::Specification.new do |s| s.name = "amqp" s.version = AMQP::VERSION s.authors = ["Aman Gupta", "Jakub Stastny aka botanicus", "Michael S. Klishin"] s.homepage = "http://rubyamqp.info" s.summary = "Mature EventMachine-based RabbitMQ client" s.description = "Mature EventMachine-based RabbitMQ client." s.email = ["bWljaGFlbEBub3ZlbWJlcmFpbi5jb20=\n", "c3Rhc3RueUAxMDFpZGVhcy5jeg==\n"].map { |i| Base64.decode64(i) } s.licenses = ["Ruby"] # files s.files = `git ls-files`.split("\n").reject { |file| file =~ /^vendor\// || file =~ /^gemfiles\// } s.require_paths = ["lib"] s.rdoc_options = '--include=examples --main README.md' s.extra_rdoc_files = ["README.md"] + Dir.glob("docs/*") # Dependencies s.add_dependency "eventmachine" s.add_dependency "amq-protocol", ">= 2.2.0" s.rubyforge_project = "amqp" end