fotonauts-bunny 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,158 @@
1
+ module Bunny
2
+
3
+ =begin rdoc
4
+
5
+ === DESCRIPTION:
6
+
7
+ *Exchanges* are the routing and distribution hub of AMQP. All messages that Bunny sends
8
+ to an AMQP broker/server _have_ to pass through an exchange in order to be routed to a
9
+ destination queue. The AMQP specification defines the types of exchange that you can create.
10
+
11
+ At the time of writing there are four (4) types of exchange defined -
12
+
13
+ * <tt>:direct</tt>
14
+ * <tt>:fanout</tt>
15
+ * <tt>:topic</tt>
16
+ * <tt>:headers</tt>
17
+
18
+ AMQP-compliant brokers/servers are required to provide default exchanges for the _direct_ and
19
+ _fanout_ exchange types. All default exchanges are prefixed with <tt>'amq.'</tt>, for example -
20
+
21
+ * <tt>amq.direct</tt>
22
+ * <tt>amq.fanout</tt>
23
+ * <tt>amq.topic</tt>
24
+ * <tt>amq.match</tt> or <tt>amq.headers</tt>
25
+
26
+ If you want more information about exchanges, please consult the documentation for your
27
+ target broker/server or visit the {AMQP website}[http://www.amqp.org] to find the version of the
28
+ specification that applies to your target broker/server.
29
+
30
+ =end
31
+
32
+ class Exchange
33
+
34
+ attr_reader :client, :type, :name, :opts, :key
35
+
36
+ def initialize(client, name, opts = {})
37
+ # check connection to server
38
+ raise Bunny::ConnectionError, 'Not connected to server' if client.status == :not_connected
39
+
40
+ @client, @name, @opts = client, name, opts
41
+
42
+ # set up the exchange type catering for default names
43
+ if name.match(/^amq\./)
44
+ new_type = name.sub(/amq\./, '')
45
+ # handle 'amq.match' default
46
+ new_type = 'headers' if new_type == 'match'
47
+ @type = new_type.to_sym
48
+ else
49
+ @type = opts[:type] || :direct
50
+ end
51
+
52
+ @key = opts[:key]
53
+ @client.exchanges[@name] ||= self
54
+
55
+ # ignore the :nowait option if passed, otherwise program will hang waiting for a
56
+ # response that will not be sent by the server
57
+ opts.delete(:nowait)
58
+
59
+ unless name == "amq.#{type}" or name == ''
60
+ client.send_frame(
61
+ Qrack::Protocol::Exchange::Declare.new(
62
+ { :exchange => name, :type => type, :nowait => false }.merge(opts)
63
+ )
64
+ )
65
+
66
+ raise Bunny::ProtocolError,
67
+ "Error declaring exchange #{name}: type = #{type}" unless
68
+ client.next_method.is_a?(Qrack::Protocol::Exchange::DeclareOk)
69
+ end
70
+ end
71
+
72
+ =begin rdoc
73
+
74
+ === DESCRIPTION:
75
+
76
+ Publishes a message to a specific exchange. The message will be routed to queues as defined
77
+ by the exchange configuration and distributed to any active consumers when the transaction,
78
+ if any, is committed.
79
+
80
+ ==== OPTIONS:
81
+
82
+ * <tt>:key => 'routing_key'</tt> - Specifies the routing key for the message. The routing key is
83
+ used for routing messages depending on the exchange configuration.
84
+ * <tt>:mandatory => true or false (_default_)</tt> - Tells the server how to react if the message
85
+ cannot be routed to a queue. If set to _true_, the server will return an unroutable message
86
+ with a Return method. If this flag is zero, the server silently drops the message.
87
+ * <tt>:immediate => true or false (_default_)</tt> - Tells the server how to react if the message
88
+ cannot be routed to a queue consumer immediately. If set to _true_, the server will return an
89
+ undeliverable message with a Return method. If set to _false_, the server will queue the message,
90
+ but with no guarantee that it will ever be consumed.
91
+
92
+ ==== RETURNS:
93
+
94
+ nil
95
+
96
+ =end
97
+
98
+ def publish(data, opts = {})
99
+ out = []
100
+
101
+ out << Qrack::Protocol::Basic::Publish.new(
102
+ { :exchange => name, :routing_key => opts.delete(:key) || key }.merge(opts)
103
+ )
104
+ data = data.to_s
105
+ out << Qrack::Protocol::Header.new(
106
+ Qrack::Protocol::Basic,
107
+ data.length, {
108
+ :content_type => 'application/octet-stream',
109
+ :delivery_mode => (opts.delete(:persistent) ? 2 : 1),
110
+ :priority => 0
111
+ }.merge(opts)
112
+ )
113
+ out << Qrack::Transport::Frame::Body.new(data)
114
+
115
+ client.send_frame(*out)
116
+ end
117
+
118
+ =begin rdoc
119
+
120
+ === DESCRIPTION:
121
+
122
+ Requests that an exchange is deleted from broker/server. Removes reference from exchanges
123
+ if successful. If an error occurs raises _Bunny_::_ProtocolError_.
124
+
125
+ ==== Options:
126
+
127
+ * <tt>:if_unused => true or false (_default_)</tt> - If set to _true_, the server will only
128
+ delete the exchange if it has no queue bindings. If the exchange has queue bindings the
129
+ server does not delete it but raises a channel exception instead.
130
+ * <tt>:nowait => true or false (_default_)</tt> - Ignored by Bunny, always _false_.
131
+
132
+ ==== Returns:
133
+
134
+ <tt>:delete_ok</tt> if successful
135
+ =end
136
+
137
+ def delete(opts = {})
138
+ # ignore the :nowait option if passed, otherwise program will hang waiting for a
139
+ # response that will not be sent by the server
140
+ opts.delete(:nowait)
141
+
142
+ client.send_frame(
143
+ Qrack::Protocol::Exchange::Delete.new({ :exchange => name, :nowait => false }.merge(opts))
144
+ )
145
+
146
+ raise Bunny::ProtocolError,
147
+ "Error deleting exchange #{name}" unless
148
+ client.next_method.is_a?(Qrack::Protocol::Exchange::DeleteOk)
149
+
150
+ client.exchanges.delete(name)
151
+
152
+ # return confirmation
153
+ :delete_ok
154
+ end
155
+
156
+ end
157
+
158
+ end
@@ -0,0 +1,410 @@
1
+ module Bunny
2
+
3
+ =begin rdoc
4
+
5
+ === DESCRIPTION:
6
+
7
+ Queues store and forward messages. Queues can be configured in the server or created at runtime.
8
+ Queues must be attached to at least one exchange in order to receive messages from publishers.
9
+
10
+ =end
11
+
12
+ class Queue
13
+ attr_reader :name, :client
14
+ attr_accessor :delivery_tag
15
+
16
+ def initialize(client, name, opts = {})
17
+ # check connection to server
18
+ raise Bunny::ConnectionError, 'Not connected to server' if client.status == :not_connected
19
+
20
+ @client = client
21
+ @opts = opts
22
+ @name = name
23
+ @delivery_tag = nil
24
+
25
+ # ignore the :nowait option if passed, otherwise program will hang waiting for a
26
+ # response that will not be sent by the server
27
+ opts.delete(:nowait)
28
+
29
+ client.send_frame(
30
+ Qrack::Protocol::Queue::Declare.new({ :queue => name, :nowait => false }.merge(opts))
31
+ )
32
+
33
+ raise Bunny::ProtocolError, "Error declaring queue #{name}" unless client.next_method.is_a?(Qrack::Protocol::Queue::DeclareOk)
34
+ end
35
+
36
+ =begin rdoc
37
+
38
+ === DESCRIPTION:
39
+
40
+ Acknowledges one or more messages delivered via the _Deliver_ or _Get_-_Ok_ methods. The client can
41
+ ask to confirm a single message or a set of messages up to and including a specific message.
42
+
43
+ ==== OPTIONS:
44
+
45
+ * <tt>:delivery_tag</tt>
46
+ * <tt>:multiple => true or false (_default_)</tt> - If set to _true_, the delivery tag is treated
47
+ as "up to and including", so that the client can acknowledge multiple messages with a single
48
+ method. If set to _false_, the delivery tag refers to a single message. If the multiple field
49
+ is _true_, and the delivery tag is zero, tells the server to acknowledge all outstanding messages.
50
+
51
+ =end
52
+
53
+ def ack
54
+ client.send_frame(
55
+ Qrack::Protocol::Basic::Ack.new(:delivery_tag => delivery_tag)
56
+ )
57
+
58
+ # reset delivery tag
59
+ self.delivery_tag = nil
60
+ end
61
+
62
+ =begin rdoc
63
+
64
+ === DESCRIPTION:
65
+
66
+ Gets a message from a queue in a synchronous way. If error occurs, raises _Bunny_::_ProtocolError_.
67
+
68
+ ==== OPTIONS:
69
+
70
+ * <tt>:header => true or false (_default_)</tt> - If set to _true_,
71
+ hash <tt>{:header, :delivery_details, :payload}</tt> is returned.
72
+ * <tt>:no_ack => true (_default_) or false</tt> - If set to _true_, the server does not expect an
73
+ acknowledgement message from the client. If set to _false_, the server expects an acknowledgement
74
+ message from the client and will re-queue the message if it does not receive one within a time specified
75
+ by the server.
76
+
77
+ ==== RETURNS:
78
+
79
+ If <tt>:header => true</tt> returns hash <tt>{:header, :delivery_details, :payload}</tt>. <tt>:delivery_details</tt> is
80
+ a hash <tt>{:delivery_tag, :redelivered, :exchange, :routing_key, :message_count}</tt>. If
81
+ <tt>:header => false</tt> only the message payload is returned.
82
+
83
+ =end
84
+
85
+ def pop(opts = {})
86
+
87
+ # do we want the message header?
88
+ hdr = opts.delete(:header)
89
+
90
+ # do we want to have to provide an acknowledgement?
91
+ ack = opts.delete(:ack)
92
+
93
+ client.send_frame(
94
+ Qrack::Protocol::Basic::Get.new({ :queue => name,
95
+ :consumer_tag => name,
96
+ :no_ack => !ack,
97
+ :nowait => true }.merge(opts))
98
+ )
99
+
100
+ method = client.next_method
101
+
102
+ if method.is_a?(Qrack::Protocol::Basic::GetEmpty) then
103
+ return :queue_empty
104
+ elsif !method.is_a?(Qrack::Protocol::Basic::GetOk)
105
+ raise Bunny::ProtocolError, "Error getting message from queue #{name}"
106
+ end
107
+
108
+ # get delivery tag to use for acknowledge
109
+ self.delivery_tag = method.delivery_tag if ack
110
+
111
+ header = client.next_payload
112
+ msg = client.next_payload
113
+ raise Bunny::MessageError, 'unexpected length' if msg.length < header.size
114
+
115
+ # Return message with additional info if requested
116
+ hdr ? {:header => header, :payload => msg, :delivery_details => method.arguments} : msg
117
+
118
+ end
119
+
120
+ =begin rdoc
121
+
122
+ === DESCRIPTION:
123
+
124
+ Publishes a message to the queue via the default nameless '' direct exchange.
125
+
126
+ ==== RETURNS:
127
+
128
+ nil
129
+
130
+ =end
131
+
132
+ def publish(data, opts = {})
133
+ exchange.publish(data, opts)
134
+ end
135
+
136
+ =begin rdoc
137
+
138
+ === DESCRIPTION:
139
+
140
+ Returns message count from Queue#status.
141
+
142
+ =end
143
+
144
+ def message_count
145
+ s = status
146
+ s[:message_count]
147
+ end
148
+
149
+ =begin rdoc
150
+
151
+ === DESCRIPTION:
152
+
153
+ Returns consumer count from Queue#status.
154
+
155
+ =end
156
+
157
+ def consumer_count
158
+ s = status
159
+ s[:consumer_count]
160
+ end
161
+
162
+ =begin rdoc
163
+
164
+ === DESCRIPTION:
165
+
166
+ Returns hash {:message_count, :consumer_count}.
167
+
168
+ =end
169
+
170
+ def status
171
+ client.send_frame(
172
+ Qrack::Protocol::Queue::Declare.new({ :queue => name, :passive => true })
173
+ )
174
+ method = client.next_method
175
+ {:message_count => method.message_count, :consumer_count => method.consumer_count}
176
+ end
177
+
178
+ =begin rdoc
179
+
180
+ === DESCRIPTION:
181
+
182
+ Asks the server to start a "consumer", which is a transient request for messages from a specific
183
+ queue. Consumers last as long as the channel they were created on, or until the client cancels them
184
+ with an _unsubscribe_. Every time a message reaches the queue it is passed to the _blk_ for
185
+ processing. If error occurs, _Bunny_::_ProtocolError_ is raised.
186
+
187
+ ==== OPTIONS:
188
+ * <tt>:header => true or false (_default_)</tt> - If set to _true_, hash is delivered for each message
189
+ <tt>{:header, :delivery_details, :payload}</tt>.
190
+ * <tt>:consumer_tag => '_tag_'</tt> - Specifies the identifier for the consumer. The consumer tag is
191
+ local to a connection, so two clients can use the same consumer tags. If this field is empty the
192
+ queue name is used.
193
+ * <tt>:no_ack=> true (_default_) or false</tt> - If set to _true_, the server does not expect an
194
+ acknowledgement message from the client. If set to _false_, the server expects an acknowledgement
195
+ message from the client and will re-queue the message if it does not receive one within a time specified
196
+ by the server.
197
+ * <tt>:exclusive => true or false (_default_)</tt> - Request exclusive consumer access, meaning
198
+ only this consumer can access the queue.
199
+ * <tt>:nowait => true or false (_default_)</tt> - Ignored by Bunny, always _false_.
200
+
201
+ ==== RETURNS:
202
+
203
+ If <tt>:header => true</tt> returns hash <tt>{:header, :delivery_details, :payload}</tt> for each message.
204
+ <tt>:delivery_details</tt> is a hash <tt>{:consumer_tag, :delivery_tag, :redelivered, :exchange, :routing_key}</tt>.
205
+ If <tt>:header => false</tt> only message payload is returned.
206
+
207
+ =end
208
+
209
+ def subscribe(opts = {}, &blk)
210
+ consumer_tag = opts[:consumer_tag] || name
211
+
212
+ # ignore the :nowait option if passed, otherwise program will hang waiting for a
213
+ # response from the server causing an error.
214
+ opts.delete(:nowait)
215
+
216
+ # do we want the message header?
217
+ hdr = opts.delete(:header)
218
+
219
+ # do we want to have to provide an acknowledgement?
220
+ ack = opts.delete(:ack)
221
+
222
+ client.send_frame(
223
+ Qrack::Protocol::Basic::Consume.new({ :queue => name,
224
+ :consumer_tag => consumer_tag,
225
+ :no_ack => !ack,
226
+ :nowait => false }.merge(opts))
227
+ )
228
+
229
+ raise Bunny::ProtocolError,
230
+ "Error subscribing to queue #{name}" unless
231
+ client.next_method.is_a?(Qrack::Protocol::Basic::ConsumeOk)
232
+
233
+ while true
234
+ method = client.next_method
235
+
236
+ break if method.is_a?(Qrack::Protocol::Basic::CancelOk)
237
+
238
+ # get delivery tag to use for acknowledge
239
+ self.delivery_tag = method.delivery_tag if ack
240
+
241
+ header = client.next_payload
242
+ msg = client.next_payload
243
+ raise Bunny::MessageError, 'unexpected length' if msg.length < header.size
244
+
245
+ # pass the message and related info, if requested, to the block for processing
246
+ blk.call(hdr ? {:header => header, :payload => msg, :delivery_details => method.arguments} : msg)
247
+ end
248
+
249
+ end
250
+
251
+ =begin rdoc
252
+
253
+ === DESCRIPTION:
254
+
255
+ Cancels a consumer. This does not affect already delivered messages, but it does mean
256
+ the server will not send any more messages for that consumer.
257
+
258
+ ==== OPTIONS:
259
+
260
+ * <tt>:consumer_tag => '_tag_'</tt> - Specifies the identifier for the consumer.
261
+ * <tt>:nowait => true or false (_default_)</tt> - Ignored by Bunny, always _false_.
262
+
263
+ =end
264
+
265
+ def unsubscribe(opts = {})
266
+ consumer_tag = opts[:consumer_tag] || name
267
+
268
+ # ignore the :nowait option if passed, otherwise program will hang waiting for a
269
+ # response from the server causing an error
270
+ opts.delete(:nowait)
271
+
272
+ client.send_frame( Qrack::Protocol::Basic::Cancel.new({ :consumer_tag => consumer_tag }.merge(opts)))
273
+
274
+ end
275
+
276
+ =begin rdoc
277
+
278
+ === DESCRIPTION:
279
+
280
+ Binds a queue to an exchange. Until a queue is bound it will not receive any messages. Queues are
281
+ bound to the direct exchange '' by default. If error occurs, a _Bunny_::_ProtocolError_ is raised.
282
+
283
+ * <tt>:key => 'routing key'* <tt>:key => 'routing_key'</tt> - Specifies the routing key for
284
+ the binding. The routing key is used for routing messages depending on the exchange configuration.
285
+ * <tt>:nowait => true or false (_default_)</tt> - Ignored by Bunny, always _false_.
286
+
287
+ ==== RETURNS:
288
+
289
+ <tt>:bind_ok</tt> if successful.
290
+
291
+ =end
292
+
293
+ def bind(exchange, opts = {})
294
+ exchange = exchange.respond_to?(:name) ? exchange.name : exchange
295
+
296
+ # ignore the :nowait option if passed, otherwise program will hang waiting for a
297
+ # response that will not be sent by the server
298
+ opts.delete(:nowait)
299
+
300
+ bindings[exchange] = opts
301
+ client.send_frame(
302
+ Qrack::Protocol::Queue::Bind.new({ :queue => name,
303
+ :exchange => exchange,
304
+ :routing_key => opts.delete(:key),
305
+ :nowait => false }.merge(opts))
306
+ )
307
+
308
+ raise Bunny::ProtocolError,
309
+ "Error binding queue #{name}" unless
310
+ client.next_method.is_a?(Qrack::Protocol::Queue::BindOk)
311
+
312
+ # return message
313
+ :bind_ok
314
+ end
315
+
316
+ =begin rdoc
317
+
318
+ === DESCRIPTION:
319
+
320
+ Removes a queue binding from an exchange. If error occurs, a _Bunny_::_ProtocolError_ is raised.
321
+
322
+ ==== OPTIONS:
323
+ * <tt>:key => 'routing key'* <tt>:key => 'routing_key'</tt> - Specifies the routing key for
324
+ the binding.
325
+ * <tt>:nowait => true or false (_default_)</tt> - Ignored by Bunny, always _false_.
326
+
327
+ ==== RETURNS:
328
+
329
+ <tt>:unbind_ok</tt> if successful.
330
+
331
+ =end
332
+
333
+ def unbind(exchange, opts = {})
334
+ exchange = exchange.respond_to?(:name) ? exchange.name : exchange
335
+
336
+ # ignore the :nowait option if passed, otherwise program will hang waiting for a
337
+ # response that will not be sent by the server
338
+ opts.delete(:nowait)
339
+
340
+ bindings.delete(exchange)
341
+
342
+ client.send_frame(
343
+ Qrack::Protocol::Queue::Unbind.new({ :queue => name,
344
+ :exchange => exchange,
345
+ :routing_key => opts.delete(:key),
346
+ :nowait => false }.merge(opts)
347
+ )
348
+ )
349
+
350
+ raise Bunny::ProtocolError,
351
+ "Error unbinding queue #{name}" unless
352
+ client.next_method.is_a?(Qrack::Protocol::Queue::UnbindOk)
353
+
354
+ # return message
355
+ :unbind_ok
356
+ end
357
+
358
+ =begin rdoc
359
+
360
+ === DESCRIPTION:
361
+
362
+ Requests that a queue is deleted from broker/server. When a queue is deleted any pending messages
363
+ are sent to a dead-letter queue if this is defined in the server configuration. Removes reference
364
+ from queues if successful. If an error occurs raises _Bunny_::_ProtocolError_.
365
+
366
+ ==== Options:
367
+
368
+ * <tt>:if_unused => true or false (_default_)</tt> - If set to _true_, the server will only
369
+ delete the queue if it has no consumers. If the queue has consumers the server does not
370
+ delete it but raises a channel exception instead.
371
+ * <tt>:if_empty => true or false (_default_)</tt> - If set to _true_, the server will only
372
+ delete the queue if it has no messages. If the queue is not empty the server raises a channel
373
+ exception.
374
+ * <tt>:nowait => true or false (_default_)</tt> - Ignored by Bunny, always _false_.
375
+
376
+ ==== Returns:
377
+
378
+ <tt>:delete_ok</tt> if successful
379
+ =end
380
+
381
+ def delete(opts = {})
382
+ # ignore the :nowait option if passed, otherwise program will hang waiting for a
383
+ # response that will not be sent by the server
384
+ opts.delete(:nowait)
385
+
386
+ client.send_frame(
387
+ Qrack::Protocol::Queue::Delete.new({ :queue => name, :nowait => false }.merge(opts))
388
+ )
389
+
390
+ raise Bunny::ProtocolError,
391
+ "Error deleting queue #{name}" unless
392
+ client.next_method.is_a?(Qrack::Protocol::Queue::DeleteOk)
393
+
394
+ client.queues.delete(name)
395
+
396
+ # return confirmation
397
+ :delete_ok
398
+ end
399
+
400
+ private
401
+ def exchange
402
+ @exchange ||= Bunny::Exchange.new(client, '', {:type => :direct, :key => name})
403
+ end
404
+
405
+ def bindings
406
+ @bindings ||= {}
407
+ end
408
+ end
409
+
410
+ end
data/lib/bunny.rb ADDED
@@ -0,0 +1,37 @@
1
+ $:.unshift File.expand_path(File.dirname(__FILE__))
2
+
3
+ # Ruby standard libraries
4
+ %w[socket thread timeout].each do |file|
5
+ require file
6
+ end
7
+
8
+ require 'qrack/qrack'
9
+
10
+ require 'bunny/client'
11
+ require 'bunny/exchange'
12
+ require 'bunny/queue'
13
+
14
+ module Bunny
15
+
16
+ include Qrack
17
+
18
+ class ProtocolError < StandardError; end
19
+ class ServerDownError < StandardError; end
20
+ class ConnectionError < StandardError; end
21
+ class MessageError < StandardError; end
22
+
23
+ VERSION = '0.4.0'
24
+
25
+ # Returns the Bunny version number
26
+
27
+ def self.version
28
+ VERSION
29
+ end
30
+
31
+ # Instantiates new Bunny::Client
32
+
33
+ def self.new(opts = {})
34
+ Bunny::Client.new(opts)
35
+ end
36
+
37
+ end
@@ -0,0 +1,5 @@
1
+ module Qrack
2
+ # Client ancestor class
3
+ class Client
4
+ end
5
+ end