amqp 0.6.7 → 0.7.0.pre

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,132 @@
1
+ # encoding: utf-8
2
+
3
+ class MQ
4
+ class Collection < ::Array
5
+ class IncompatibleItemError < ArgumentError
6
+ def initialize(item)
7
+ super("Instance of #{item.class} doesn't respond to #name!")
8
+ end
9
+ end
10
+
11
+ def [](name)
12
+ self.find do |object|
13
+ object.name == name
14
+ end
15
+ end
16
+
17
+ # Collection#[]= doesn't really make any sense, as we can't
18
+ # redefine already existing Queues and Exchanges (we can declare
19
+ # them multiple times, but if the queue resp. exchange is already
20
+ # in the collection, it doesn't make sense to do so and we can't
21
+ # run declare twice in order to change options, because the AMQP
22
+ # broker closes the connection if we try to do so).
23
+
24
+ # Use Collection#<< for adding items to the collection.
25
+ undef_method :[]=
26
+
27
+ def <<(item)
28
+ if (item.name rescue nil).nil? || ! self[item.name]
29
+ self.add!(item)
30
+ end
31
+
32
+ return item
33
+ end
34
+
35
+ alias_method :__push__, :push
36
+ alias_method :push, :<<
37
+
38
+ def add!(item)
39
+ unless item.respond_to?(:name)
40
+ raise IncompatibleItemError.new(item)
41
+ end
42
+
43
+ __push__(item)
44
+ return item
45
+ end
46
+ end
47
+ end
48
+
49
+ if $0 =~ /bacon/ or $0 == __FILE__
50
+ require "bacon"
51
+
52
+ Item = Struct.new(:name)
53
+
54
+ describe MQ::Collection do
55
+ before do
56
+ @items = 3.times.map { |int| Item.new("name-#{int}") }
57
+ @collection = MQ::Collection.new(@items)
58
+ end
59
+
60
+ describe "accessors" do
61
+ should "be accessible by its name" do
62
+ @collection["name-1"].should.not.be.nil
63
+ @collection["name-1"].should.eql(@items[1])
64
+ end
65
+
66
+ should "not allow to change already existing object" do
67
+ lambda { @collection["name-1"] = Item.new("test") }.should.raise(NoMethodError)
68
+ end
69
+ end
70
+
71
+ describe "#<<" do
72
+ should "raise IncompatibleItemError if the argument doesn't have method :name" do
73
+ lambda { @collection << nil }.should.raise(MQ::Collection::IncompatibleItemError)
74
+ end
75
+
76
+ should "add an item into the collection" do
77
+ length = @collection.length
78
+ @collection << Item.new("test")
79
+ @collection.length.should.eql(length + 1)
80
+ end
81
+
82
+ should "not add an item to the collection if another item with given name already exists and the name IS NOT nil" do
83
+ @collection << Item.new("test")
84
+ length = @collection.length
85
+ @collection << Item.new("test")
86
+ @collection.length.should.eql(length)
87
+ end
88
+
89
+ should "add an item to the collection if another item with given name already exists and the name IS nil" do
90
+ @collection << Item.new(nil)
91
+ length = @collection.length
92
+ @collection << Item.new(nil)
93
+ @collection.length.should.eql(length + 1)
94
+ end
95
+
96
+ should "return the item" do
97
+ item = Item.new("test")
98
+ (@collection << item).should.eql item
99
+ end
100
+
101
+ should "return the item even if it already existed" do
102
+ item = Item.new("test")
103
+ @collection << item
104
+ (@collection << item).should.eql item
105
+ end
106
+ end
107
+
108
+ describe "#add!" do
109
+ should "raise IncompatibleItemError if the argument doesn't have method :name" do
110
+ lambda { @collection << nil }.should.raise(MQ::Collection::IncompatibleItemError)
111
+ end
112
+
113
+ should "add an item into the collection" do
114
+ length = @collection.length
115
+ @collection << Item.new("test")
116
+ @collection.length.should.eql(length + 1)
117
+ end
118
+
119
+ should "add an item to the collection if another item with given name already exists" do
120
+ @collection.add! Item.new("test")
121
+ length = @collection.length
122
+ @collection.add! Item.new("test")
123
+ @collection.length.should.eql(length + 1)
124
+ end
125
+
126
+ should "return the item" do
127
+ item = Item.new("test")
128
+ (@collection << item).should.eql item
129
+ end
130
+ end
131
+ end
132
+ end
@@ -10,7 +10,7 @@ class MQ
10
10
  # There are three (3) supported Exchange types: direct, fanout and topic.
11
11
  #
12
12
  # As part of the standard, the server _must_ predeclare the direct exchange
13
- # 'amq.direct' and the fanout exchange 'amq.fanout' (all exchange names
13
+ # 'amq.direct' and the fanout exchange 'amq.fanout' (all exchange names
14
14
  # starting with 'amq.' are reserved). Attempts to declare an exchange using
15
15
  # 'amq.' as the name will raise an MQ:Error and fail. In practice these
16
16
  # default exchanges are never used directly by client code.
@@ -28,7 +28,7 @@ class MQ
28
28
  # There are three (3) supported Exchange types: direct, fanout and topic.
29
29
  #
30
30
  # As part of the standard, the server _must_ predeclare the direct exchange
31
- # 'amq.direct' and the fanout exchange 'amq.fanout' (all exchange names
31
+ # 'amq.direct' and the fanout exchange 'amq.fanout' (all exchange names
32
32
  # starting with 'amq.' are reserved). Attempts to declare an exchange using
33
33
  # 'amq.' as the name will raise an MQ:Error and fail. In practice these
34
34
  # default exchanges are never used directly by client code.
@@ -36,8 +36,8 @@ class MQ
36
36
  # == Direct
37
37
  # A direct exchange is useful for 1:1 communication between a publisher and
38
38
  # subscriber. Messages are routed to the queue with a binding that shares
39
- # the same name as the exchange. Alternately, the messages are routed to
40
- # the bound queue that shares the same name as the routing key used for
39
+ # the same name as the exchange. Alternately, the messages are routed to
40
+ # the bound queue that shares the same name as the routing key used for
41
41
  # defining the exchange. This exchange type does not honor the :key option
42
42
  # when defining a new instance with a name. It _will_ honor the :key option
43
43
  # if the exchange name is the empty string. This is because an exchange
@@ -57,14 +57,14 @@ class MQ
57
57
  # queue.pop { |data| puts "received data [#{data}]" }
58
58
  #
59
59
  # == Fanout
60
- # A fanout exchange is useful for 1:N communication where one publisher
61
- # feeds multiple subscribers. Like direct exchanges, messages published
62
- # to a fanout exchange are delivered to queues whose name matches the
63
- # exchange name (or are bound to that exchange name). Each queue gets
60
+ # A fanout exchange is useful for 1:N communication where one publisher
61
+ # feeds multiple subscribers. Like direct exchanges, messages published
62
+ # to a fanout exchange are delivered to queues whose name matches the
63
+ # exchange name (or are bound to that exchange name). Each queue gets
64
64
  # its own copy of the message.
65
65
  #
66
- # Like the direct exchange type, this exchange type does not honor the
67
- # :key option when defining a new instance with a name. It _will_ honor
66
+ # Like the direct exchange type, this exchange type does not honor the
67
+ # :key option when defining a new instance with a name. It _will_ honor
68
68
  # the :key option if the exchange name is the empty string. Fanout exchanges
69
69
  # defined with the empty string as the name use the default 'amq.fanout'.
70
70
  # In this case it needs to use :key to do its matching.
@@ -91,20 +91,20 @@ class MQ
91
91
  # end
92
92
  #
93
93
  # == Topic
94
- # A topic exchange allows for messages to be published to an exchange
94
+ # A topic exchange allows for messages to be published to an exchange
95
95
  # tagged with a specific routing key. The Exchange uses the routing key
96
- # to determine which queues to deliver the message. Wildcard matching
97
- # is allowed. The topic must be declared using dot notation to separate
96
+ # to determine which queues to deliver the message. Wildcard matching
97
+ # is allowed. The topic must be declared using dot notation to separate
98
98
  # each subtopic.
99
99
  #
100
100
  # This is the only exchange type to honor the :key parameter.
101
101
  #
102
- # As part of the AMQP standard, each server _should_ predeclare a topic
102
+ # As part of the AMQP standard, each server _should_ predeclare a topic
103
103
  # exchange called 'amq.topic' (this is not required by the standard).
104
104
  #
105
105
  # The classic example is delivering market data. When publishing market
106
- # data for stocks, we may subdivide the stream based on 2
107
- # characteristics: nation code and trading symbol. The topic tree for
106
+ # data for stocks, we may subdivide the stream based on 2
107
+ # characteristics: nation code and trading symbol. The topic tree for
108
108
  # Apple Computer would look like:
109
109
  # 'stock.us.aapl'
110
110
  # For a foreign stock, it may look like:
@@ -139,10 +139,10 @@ class MQ
139
139
  # end
140
140
  # end
141
141
  #
142
- # For matching, the '*' (asterisk) wildcard matches against one
143
- # dot-separated item only. The '#' wildcard (hash or pound symbol)
144
- # matches against 0 or more dot-separated items. If none of these
145
- # symbols are used, the exchange performs a comparison looking for an
142
+ # For matching, the '*' (asterisk) wildcard matches against one
143
+ # dot-separated item only. The '#' wildcard (hash or pound symbol)
144
+ # matches against 0 or more dot-separated items. If none of these
145
+ # symbols are used, the exchange performs a comparison looking for an
146
146
  # exact match.
147
147
  #
148
148
  # == Options
@@ -150,12 +150,12 @@ class MQ
150
150
  # If set, the server will not create the exchange if it does not
151
151
  # already exist. The client can use this to check whether an exchange
152
152
  # exists without modifying the server state.
153
- #
153
+ #
154
154
  # * :durable => true | false (default false)
155
155
  # If set when creating a new exchange, the exchange will be marked as
156
156
  # durable. Durable exchanges remain active when a server restarts.
157
157
  # Non-durable exchanges (transient exchanges) are purged if/when a
158
- # server restarts.
158
+ # server restarts.
159
159
  #
160
160
  # A transient exchange (the default) is stored in memory-only
161
161
  # therefore it is a good choice for high-performance and low-latency
@@ -183,26 +183,56 @@ class MQ
183
183
  # not wait for a reply method. If the server could not complete the
184
184
  # method it will raise a channel or connection exception.
185
185
  #
186
+ # * :no_declare => true | false (default false)
187
+ # If set, the exchange will not be declared to the
188
+ # AMQP broker at instantiation-time. This allows the AMQP
189
+ # client to send messages to exchanges that were
190
+ # already declared by someone else, e.g. if the client
191
+ # does not have sufficient privilege to declare (create)
192
+ # an exchange. Use with caution, as binding to an exchange
193
+ # with the no-declare option causes your system to become
194
+ # sensitive to the ordering of clients' actions!
195
+ #
186
196
  # == Exceptions
187
197
  # Doing any of these activities are illegal and will raise MQ:Error.
188
198
  # * redeclare an already-declared exchange to a different type
189
199
  # * :passive => true and the exchange does not exist (NOT_FOUND)
190
200
  #
191
- def initialize mq, type, name, opts = {}
201
+ def initialize mq, type, name, opts = {}, &block
192
202
  @mq = mq
193
- @type, @name, @opts = type, name, opts
194
- @mq.exchanges[@name = name] ||= self
203
+ @type, @opts = type, opts
204
+ @opts = { :exchange => name, :type => type, :nowait => block.nil? }.merge(opts)
195
205
  @key = opts[:key]
196
-
206
+ @name = name unless name.empty?
207
+ @status = :unknown
208
+
209
+ # The AMQP 0.8 specification (as well as 0.9.1) in 1.1.4.2 mentiones
210
+ # that Exchange.Declare-Ok confirms the name of the exchange (because
211
+ # of automatically­named), which is logical to interpret that this
212
+ # functionality should be the same as for Queue (though it isn't
213
+ # explicitely told in the specification). In fact, RabbitMQ (and
214
+ # probably other implementations as well) doesn't support it and
215
+ # there is a default exchange with an empty name (so-called default
216
+ # or nameless exchange), so if we'd send Exchange.Declare(exchange=""),
217
+ # then RabbitMQ interpret it as if we'd try to redefine this default
218
+ # exchange so it'd produce an error.
197
219
  unless name == "amq.#{type}" or name == '' or opts[:no_declare]
198
- @mq.callback{
199
- @mq.send Protocol::Exchange::Declare.new({ :exchange => name,
200
- :type => type,
201
- :nowait => true }.merge(opts))
220
+ @status = :unfinished
221
+ @mq.callback {
222
+ @mq.send Protocol::Exchange::Declare.new(@opts)
202
223
  }
224
+ else
225
+ # Call the callback immediately, as given exchange is already
226
+ # declared.
227
+ @status = :finished
228
+ block.call(self)
203
229
  end
230
+
231
+ self.callback = block
204
232
  end
205
- attr_reader :name, :type, :key
233
+
234
+ attr_reader :name, :type, :key, :status
235
+ attr_accessor :opts, :callback
206
236
 
207
237
  # This method publishes a staged file message to a specific exchange.
208
238
  # The file message will be routed to queues as defined by the exchange
@@ -212,7 +242,7 @@ class MQ
212
242
  # exchange = MQ.direct('name', :key => 'foo.bar')
213
243
  # exchange.publish("some data")
214
244
  #
215
- # The method takes several hash key options which modify the behavior or
245
+ # The method takes several hash key options which modify the behavior or
216
246
  # lifecycle of the message.
217
247
  #
218
248
  # * :routing_key => 'string'
@@ -236,7 +266,7 @@ class MQ
236
266
  # no guarantee that it will ever be consumed.
237
267
  #
238
268
  # * :persistent
239
- # True or False. When true, this message will remain in the queue until
269
+ # True or False. When true, this message will remain in the queue until
240
270
  # it is consumed (if the queue is durable). When false, the message is
241
271
  # lost if the server restarts and the queue is recreated.
242
272
  #
@@ -254,7 +284,7 @@ class MQ
254
284
  data = data.to_s
255
285
 
256
286
  out << Protocol::Header.new(Protocol::Basic,
257
- data.length, { :content_type => 'application/octet-stream',
287
+ data.bytesize, { :content_type => 'application/octet-stream',
258
288
  :delivery_mode => (opts[:persistent] ? 2 : 1),
259
289
  :priority => 0 }.merge(opts))
260
290
 
@@ -286,7 +316,7 @@ class MQ
286
316
  # If set, the server will only delete the exchange if it has no queue
287
317
  # bindings. If the exchange has queue bindings the server does not
288
318
  # delete it but raises a channel exception instead (MQ:Error).
289
- #
319
+ #
290
320
  def delete opts = {}
291
321
  @mq.callback{
292
322
  @mq.send Protocol::Exchange::Delete.new({ :exchange => name,
@@ -300,5 +330,9 @@ class MQ
300
330
  @deferred_status = nil
301
331
  initialize @mq, @type, @name, @opts
302
332
  end
333
+
334
+ def receive_response response
335
+ self.callback && self.callback.call(self)
336
+ end
303
337
  end
304
- end
338
+ end
@@ -1,7 +1,7 @@
1
1
  class MQ
2
2
  class Queue
3
3
  include AMQP
4
-
4
+
5
5
  # Queues store and forward messages. Queues can be configured in the server
6
6
  # or created at runtime. Queues must be attached to at least one exchange
7
7
  # in order to receive messages from publishers.
@@ -10,7 +10,7 @@ class MQ
10
10
  # internal use. Attempts to create queue names in violation of this
11
11
  # reservation will raise MQ:Error (ACCESS_REFUSED).
12
12
  #
13
- # When a queue is created without a name, the server will generate a
13
+ # When a queue is created without a name, the server will generate a
14
14
  # unique name internally (not currently supported in this library).
15
15
  #
16
16
  # == Options
@@ -18,7 +18,7 @@ class MQ
18
18
  # If set, the server will not create the exchange if it does not
19
19
  # already exist. The client can use this to check whether an exchange
20
20
  # exists without modifying the server state.
21
- #
21
+ #
22
22
  # * :durable => true | false (default false)
23
23
  # If set when creating a new queue, the queue will be marked as
24
24
  # durable. Durable queues remain active when a server restarts.
@@ -47,7 +47,7 @@ class MQ
47
47
  # If set, the queue is deleted when all consumers have finished
48
48
  # using it. Last consumer can be cancelled either explicitly or because
49
49
  # its channel is closed. If there was no consumer ever on the queue, it
50
- # won't be deleted.
50
+ # won't be deleted.
51
51
  #
52
52
  # The server waits for a short period of time before
53
53
  # determining the queue is unused to give time to the client code
@@ -61,17 +61,21 @@ class MQ
61
61
  # not wait for a reply method. If the server could not complete the
62
62
  # method it will raise a channel or connection exception.
63
63
  #
64
- def initialize mq, name, opts = {}
64
+ def initialize mq, name, opts = {}, &block
65
65
  @mq = mq
66
- @opts = opts
66
+ @opts = { :queue => name, :nowait => block.nil? }.merge(opts)
67
67
  @bindings ||= {}
68
- @mq.queues[@name = name] ||= self
69
- @mq.callback{
70
- @mq.send Protocol::Queue::Declare.new({ :queue => name,
71
- :nowait => true }.merge(opts))
68
+ @name = name unless name.empty?
69
+ @status = @opts[:nowait] ? :unknown : :unfinished
70
+ @mq.callback {
71
+ @mq.send Protocol::Queue::Declare.new(@opts)
72
72
  }
73
+
74
+ self.callback = block
73
75
  end
76
+
74
77
  attr_reader :name
78
+ attr_accessor :opts, :callback, :bind_callback
75
79
 
76
80
  # This method binds a queue to an exchange. Until a queue is
77
81
  # bound it will not receive any messages. In a classic messaging
@@ -106,7 +110,8 @@ class MQ
106
110
  # not wait for a reply method. If the server could not complete the
107
111
  # method it will raise a channel or connection exception.
108
112
  #
109
- def bind exchange, opts = {}
113
+ def bind exchange, opts = {}, &block
114
+ @status = :unbound
110
115
  exchange = exchange.respond_to?(:name) ? exchange.name : exchange
111
116
  @bindings[exchange] = opts
112
117
 
@@ -114,13 +119,14 @@ class MQ
114
119
  @mq.send Protocol::Queue::Bind.new({ :queue => name,
115
120
  :exchange => exchange,
116
121
  :routing_key => opts[:key],
117
- :nowait => true }.merge(opts))
122
+ :nowait => block.nil? }.merge(opts))
118
123
  }
124
+ self.bind_callback = block
119
125
  self
120
126
  end
121
127
 
122
128
  # Remove the binding between the queue and exchange. The queue will
123
- # not receive any more messages until it is bound to another
129
+ # not receive any more messages until it is bound to another
124
130
  # exchange.
125
131
  #
126
132
  # Due to the asynchronous nature of the protocol, it is possible for
@@ -197,7 +203,7 @@ class MQ
197
203
  # EM.add_periodic_timer(1) do
198
204
  # exchange.publish("random number #{rand(1000)}")
199
205
  # end
200
- #
206
+ #
201
207
  # # note that #bind is never called; it is implicit because
202
208
  # # the exchange and queue names match
203
209
  # queue = MQ.queue('foo queue')
@@ -215,9 +221,9 @@ class MQ
215
221
  # EM.add_periodic_timer(1) do
216
222
  # exchange.publish("random number #{rand(1000)}")
217
223
  # end
218
- #
224
+ #
219
225
  # queue = MQ.queue('foo queue')
220
- # queue.pop do |header, body|
226
+ # queue.pop do |header, body|
221
227
  # p header
222
228
  # puts "received payload [#{body}]"
223
229
  # end
@@ -268,7 +274,7 @@ class MQ
268
274
  # EM.add_periodic_timer(1) do
269
275
  # exchange.publish("random number #{rand(1000)}")
270
276
  # end
271
- #
277
+ #
272
278
  # queue = MQ.queue('foo queue')
273
279
  # queue.subscribe { |body| puts "received payload [#{body}]" }
274
280
  # end
@@ -282,11 +288,11 @@ class MQ
282
288
  # EM.add_periodic_timer(1) do
283
289
  # exchange.publish("random number #{rand(1000)}")
284
290
  # end
285
- #
291
+ #
286
292
  # # note that #bind is never called; it is implicit because
287
293
  # # the exchange and queue names match
288
294
  # queue = MQ.queue('foo queue')
289
- # queue.subscribe do |header, body|
295
+ # queue.subscribe do |header, body|
290
296
  # p header
291
297
  # puts "received payload [#{body}]"
292
298
  # end
@@ -340,11 +346,11 @@ class MQ
340
346
  # Those messages will be serviced by the last block used in a
341
347
  # #subscribe or #pop call.
342
348
  #
343
- # Additionally, if the queue was created with _autodelete_ set to
349
+ # Additionally, if the queue was created with _autodelete_ set to
344
350
  # true, the server will delete the queue after its wait period
345
351
  # has expired unless the queue is bound to an active exchange.
346
352
  #
347
- # The method accepts a block which will be executed when the
353
+ # The method accepts a block which will be executed when the
348
354
  # unsubscription request is acknowledged as complete by the server.
349
355
  #
350
356
  # * :nowait => true | false (default true)
@@ -363,21 +369,21 @@ class MQ
363
369
  def publish data, opts = {}
364
370
  exchange.publish(data, opts)
365
371
  end
366
-
372
+
367
373
  # Boolean check to see if the current queue has already been subscribed
368
- # to an exchange.
374
+ # to an exchange.
369
375
  #
370
376
  # Attempts to #subscribe multiple times to any exchange will raise an
371
- # Exception. Only a single block at a time can be associated with any
377
+ # Exception. Only a single block at a time can be associated with any
372
378
  # one queue for processing incoming messages.
373
379
  #
374
380
  def subscribed?
375
381
  !!@on_msg
376
382
  end
377
383
 
378
- # Passes the message to the block passed to pop or subscribe.
384
+ # Passes the message to the block passed to pop or subscribe.
379
385
  #
380
- # Performs an arity check on the block's parameters. If arity == 1,
386
+ # Performs an arity check on the block's parameters. If arity == 1,
381
387
  # pass only the message body. If arity != 1, pass the headers and
382
388
  # the body to the block.
383
389
  #
@@ -385,7 +391,7 @@ class MQ
385
391
  # the headers parameter. See #pop or #subscribe for a code example.
386
392
  #
387
393
  def receive headers, body
388
- headers = MQ::Header.new(@mq, headers)
394
+ headers = MQ::Header.new(@mq, headers) unless headers.nil?
389
395
 
390
396
  if cb = (@on_msg || @on_pop)
391
397
  cb.call *(cb.arity == 1 ? [body] : [headers, body])
@@ -399,6 +405,8 @@ class MQ
399
405
  # }
400
406
  #
401
407
  def status opts = {}, &blk
408
+ return @status if opts.empty? && blk.nil?
409
+
402
410
  @on_status = blk
403
411
  @mq.callback{
404
412
  @mq.send Protocol::Queue::Declare.new({ :queue => name,
@@ -408,6 +416,13 @@ class MQ
408
416
  end
409
417
 
410
418
  def receive_status declare_ok
419
+ @name = declare_ok.queue
420
+ @status = :finished
421
+
422
+ if self.callback
423
+ self.callback.call(self, declare_ok.message_count, declare_ok.consumer_count)
424
+ end
425
+
411
426
  if @on_status
412
427
  m, c = declare_ok.message_count, declare_ok.consumer_count
413
428
  @on_status.call *(@on_status.arity == 1 ? [m] : [m, c])
@@ -415,6 +430,13 @@ class MQ
415
430
  end
416
431
  end
417
432
 
433
+ def after_bind bind_ok
434
+ @status = :bound
435
+ if self.bind_callback
436
+ self.bind_callback.call(self)
437
+ end
438
+ end
439
+
418
440
  def confirm_subscribe
419
441
  @on_confirm_subscribe.call if @on_confirm_subscribe
420
442
  @on_confirm_subscribe = nil
@@ -424,6 +446,7 @@ class MQ
424
446
  @on_cancel.call if @on_cancel
425
447
  @on_cancel = @on_msg = nil
426
448
  @mq.consumers.delete @consumer_tag
449
+ @mq.queues.delete(@name) if @opts[:auto_delete]
427
450
  @consumer_tag = nil
428
451
  end
429
452
 
@@ -444,11 +467,11 @@ class MQ
444
467
  pop @on_pop_opts, &@on_pop
445
468
  end
446
469
  end
447
-
470
+
448
471
  private
449
-
472
+
450
473
  def exchange
451
474
  @exchange ||= Exchange.new(@mq, :direct, '', :key => name)
452
475
  end
453
476
  end
454
- end
477
+ end