right_amqp 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/LICENSE +20 -0
- data/README.rdoc +216 -0
- data/Rakefile +70 -0
- data/lib/right_amqp.rb +28 -0
- data/lib/right_amqp/amqp.rb +115 -0
- data/lib/right_amqp/amqp/buffer.rb +395 -0
- data/lib/right_amqp/amqp/client.rb +282 -0
- data/lib/right_amqp/amqp/frame.rb +124 -0
- data/lib/right_amqp/amqp/protocol.rb +212 -0
- data/lib/right_amqp/amqp/server.rb +99 -0
- data/lib/right_amqp/amqp/spec.rb +832 -0
- data/lib/right_amqp/amqp/version.rb +3 -0
- data/lib/right_amqp/ext/blankslate.rb +7 -0
- data/lib/right_amqp/ext/em.rb +8 -0
- data/lib/right_amqp/ext/emfork.rb +69 -0
- data/lib/right_amqp/ha_client.rb +25 -0
- data/lib/right_amqp/ha_client/broker_client.rb +690 -0
- data/lib/right_amqp/ha_client/ha_broker_client.rb +1185 -0
- data/lib/right_amqp/mq.rb +866 -0
- data/lib/right_amqp/mq/exchange.rb +304 -0
- data/lib/right_amqp/mq/header.rb +33 -0
- data/lib/right_amqp/mq/logger.rb +89 -0
- data/lib/right_amqp/mq/queue.rb +456 -0
- data/lib/right_amqp/mq/rpc.rb +100 -0
- data/right_amqp.gemspec +57 -0
- data/spec/amqp/client_reconnect_spec.rb +105 -0
- data/spec/ha_client/broker_client_spec.rb +936 -0
- data/spec/ha_client/ha_broker_client_spec.rb +1385 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +56 -0
- metadata +141 -0
@@ -0,0 +1,304 @@
|
|
1
|
+
class MQ
|
2
|
+
# An Exchange acts as an ingress point for all published messages. An
|
3
|
+
# exchange may also be described as a router or a matcher. Every
|
4
|
+
# published message is received by an exchange which, depending on its
|
5
|
+
# type (described below), determines how to deliver the message.
|
6
|
+
#
|
7
|
+
# It determines the next delivery hop by examining the bindings associated
|
8
|
+
# with the exchange.
|
9
|
+
#
|
10
|
+
# There are three (3) supported Exchange types: direct, fanout and topic.
|
11
|
+
#
|
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
|
14
|
+
# starting with 'amq.' are reserved). Attempts to declare an exchange using
|
15
|
+
# 'amq.' as the name will raise an MQ:Error and fail. In practice these
|
16
|
+
# default exchanges are never used directly by client code.
|
17
|
+
#
|
18
|
+
# These predececlared exchanges are used when the client code declares
|
19
|
+
# an exchange without a name. In these cases the library will use
|
20
|
+
# the default exchange for publishing the messages.
|
21
|
+
#
|
22
|
+
class Exchange
|
23
|
+
include AMQP
|
24
|
+
|
25
|
+
# Defines, intializes and returns an Exchange to act as an ingress
|
26
|
+
# point for all published messages.
|
27
|
+
#
|
28
|
+
# There are three (3) supported Exchange types: direct, fanout and topic.
|
29
|
+
#
|
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
|
32
|
+
# starting with 'amq.' are reserved). Attempts to declare an exchange using
|
33
|
+
# 'amq.' as the name will raise an MQ:Error and fail. In practice these
|
34
|
+
# default exchanges are never used directly by client code.
|
35
|
+
#
|
36
|
+
# == Direct
|
37
|
+
# A direct exchange is useful for 1:1 communication between a publisher and
|
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
|
41
|
+
# defining the exchange. This exchange type does not honor the :key option
|
42
|
+
# when defining a new instance with a name. It _will_ honor the :key option
|
43
|
+
# if the exchange name is the empty string. This is because an exchange
|
44
|
+
# defined with the empty string uses the default pre-declared exchange
|
45
|
+
# called 'amq.direct'. In this case it needs to use :key to do its matching.
|
46
|
+
#
|
47
|
+
# # exchange is named 'foo'
|
48
|
+
# exchange = MQ::Exchange.new(MQ.new, :direct, 'foo')
|
49
|
+
#
|
50
|
+
# # or, the exchange can use the default name (amq.direct) and perform
|
51
|
+
# # routing comparisons using the :key
|
52
|
+
# exchange = MQ::Exchange.new(MQ.new, :direct, "", :key => 'foo')
|
53
|
+
# exchange.publish('some data') # will be delivered to queue bound to 'foo'
|
54
|
+
#
|
55
|
+
# queue = MQ::Queue.new(MQ.new, 'foo')
|
56
|
+
# # can receive data since the queue name and the exchange key match exactly
|
57
|
+
# queue.pop { |data| puts "received data [#{data}]" }
|
58
|
+
#
|
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
|
64
|
+
# its own copy of the message.
|
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
|
68
|
+
# the :key option if the exchange name is the empty string. Fanout exchanges
|
69
|
+
# defined with the empty string as the name use the default 'amq.fanout'.
|
70
|
+
# In this case it needs to use :key to do its matching.
|
71
|
+
#
|
72
|
+
# EM.run do
|
73
|
+
# clock = MQ::Exchange.new(MQ.new, :fanout, 'clock')
|
74
|
+
# EM.add_periodic_timer(1) do
|
75
|
+
# puts "\npublishing #{time = Time.now}"
|
76
|
+
# clock.publish(Marshal.dump(time))
|
77
|
+
# end
|
78
|
+
#
|
79
|
+
# # one way of defining a queue
|
80
|
+
# amq = MQ::Queue.new(MQ.new, 'every second')
|
81
|
+
# amq.bind(MQ.fanout('clock')).subscribe do |time|
|
82
|
+
# puts "every second received #{Marshal.load(time)}"
|
83
|
+
# end
|
84
|
+
#
|
85
|
+
# # defining a queue using the convenience method
|
86
|
+
# # note the string passed to #bind
|
87
|
+
# MQ.queue('every 5 seconds').bind('clock').subscribe do |time|
|
88
|
+
# time = Marshal.load(time)
|
89
|
+
# puts "every 5 seconds received #{time}" if time.strftime('%S').to_i%5 == 0
|
90
|
+
# end
|
91
|
+
# end
|
92
|
+
#
|
93
|
+
# == Topic
|
94
|
+
# A topic exchange allows for messages to be published to an exchange
|
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
|
98
|
+
# each subtopic.
|
99
|
+
#
|
100
|
+
# This is the only exchange type to honor the :key parameter.
|
101
|
+
#
|
102
|
+
# As part of the AMQP standard, each server _should_ predeclare a topic
|
103
|
+
# exchange called 'amq.topic' (this is not required by the standard).
|
104
|
+
#
|
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
|
108
|
+
# Apple Computer would look like:
|
109
|
+
# 'stock.us.aapl'
|
110
|
+
# For a foreign stock, it may look like:
|
111
|
+
# 'stock.de.dax'
|
112
|
+
#
|
113
|
+
# When publishing data to the exchange, bound queues subscribing to the
|
114
|
+
# exchange indicate which data interests them by passing a routing key
|
115
|
+
# for matching against the published routing key.
|
116
|
+
#
|
117
|
+
# EM.run do
|
118
|
+
# exch = MQ::Exchange.new(MQ.new, :topic, "stocks")
|
119
|
+
# keys = ['stock.us.aapl', 'stock.de.dax']
|
120
|
+
#
|
121
|
+
# EM.add_periodic_timer(1) do # every second
|
122
|
+
# puts
|
123
|
+
# exch.publish(10+rand(10), :routing_key => keys[rand(2)])
|
124
|
+
# end
|
125
|
+
#
|
126
|
+
# # match against one dot-separated item
|
127
|
+
# MQ.queue('us stocks').bind(exch, :key => 'stock.us.*').subscribe do |price|
|
128
|
+
# puts "us stock price [#{price}]"
|
129
|
+
# end
|
130
|
+
#
|
131
|
+
# # match against multiple dot-separated items
|
132
|
+
# MQ.queue('all stocks').bind(exch, :key => 'stock.#').subscribe do |price|
|
133
|
+
# puts "all stocks: price [#{price}]"
|
134
|
+
# end
|
135
|
+
#
|
136
|
+
# # require exact match
|
137
|
+
# MQ.queue('only dax').bind(exch, :key => 'stock.de.dax').subscribe do |price|
|
138
|
+
# puts "dax price [#{price}]"
|
139
|
+
# end
|
140
|
+
# end
|
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
|
146
|
+
# exact match.
|
147
|
+
#
|
148
|
+
# == Options
|
149
|
+
# * :passive => true | false (default false)
|
150
|
+
# If set, the server will not create the exchange if it does not
|
151
|
+
# already exist. The client can use this to check whether an exchange
|
152
|
+
# exists without modifying the server state.
|
153
|
+
#
|
154
|
+
# * :durable => true | false (default false)
|
155
|
+
# If set when creating a new exchange, the exchange will be marked as
|
156
|
+
# durable. Durable exchanges remain active when a server restarts.
|
157
|
+
# Non-durable exchanges (transient exchanges) are purged if/when a
|
158
|
+
# server restarts.
|
159
|
+
#
|
160
|
+
# A transient exchange (the default) is stored in memory-only
|
161
|
+
# therefore it is a good choice for high-performance and low-latency
|
162
|
+
# message publishing.
|
163
|
+
#
|
164
|
+
# Durable exchanges cause all messages to be written to non-volatile
|
165
|
+
# backing store (i.e. disk) prior to routing to any bound queues.
|
166
|
+
#
|
167
|
+
# * :auto_delete => true | false (default false)
|
168
|
+
# If set, the exchange is deleted when all queues have finished
|
169
|
+
# using it. The server waits for a short period of time before
|
170
|
+
# determining the exchange is unused to give time to the client code
|
171
|
+
# to bind a queue to it.
|
172
|
+
#
|
173
|
+
# If the exchange has been previously declared, this option is ignored
|
174
|
+
# on subsequent declarations.
|
175
|
+
#
|
176
|
+
# * :internal => true | false (default false)
|
177
|
+
# If set, the exchange may not be used directly by publishers, but
|
178
|
+
# only when bound to other exchanges. Internal exchanges are used to
|
179
|
+
# construct wiring that is not visible to applications.
|
180
|
+
#
|
181
|
+
# * :nowait => true | false (default true)
|
182
|
+
# If set, the server will not respond to the method. The client should
|
183
|
+
# not wait for a reply method. If the server could not complete the
|
184
|
+
# method it will raise a channel or connection exception.
|
185
|
+
#
|
186
|
+
# == Exceptions
|
187
|
+
# Doing any of these activities are illegal and will raise MQ:Error.
|
188
|
+
# * redeclare an already-declared exchange to a different type
|
189
|
+
# * :passive => true and the exchange does not exist (NOT_FOUND)
|
190
|
+
#
|
191
|
+
def initialize mq, type, name, opts = {}
|
192
|
+
@mq = mq
|
193
|
+
@type, @name, @opts = type, name, opts
|
194
|
+
@mq.exchanges[@name = name] ||= self
|
195
|
+
@key = opts[:key]
|
196
|
+
|
197
|
+
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))
|
202
|
+
}
|
203
|
+
end
|
204
|
+
end
|
205
|
+
attr_reader :name, :type, :key
|
206
|
+
|
207
|
+
# This method publishes a staged file message to a specific exchange.
|
208
|
+
# The file message will be routed to queues as defined by the exchange
|
209
|
+
# configuration and distributed to any active consumers when the
|
210
|
+
# transaction, if any, is committed.
|
211
|
+
#
|
212
|
+
# exchange = MQ.direct('name', :key => 'foo.bar')
|
213
|
+
# exchange.publish("some data")
|
214
|
+
#
|
215
|
+
# The method takes several hash key options which modify the behavior or
|
216
|
+
# lifecycle of the message.
|
217
|
+
#
|
218
|
+
# * :routing_key => 'string'
|
219
|
+
#
|
220
|
+
# Specifies the routing key for the message. The routing key is
|
221
|
+
# used for routing messages depending on the exchange configuration.
|
222
|
+
#
|
223
|
+
# * :mandatory => true | false (default false)
|
224
|
+
#
|
225
|
+
# This flag tells the server how to react if the message cannot be
|
226
|
+
# routed to a queue. If this flag is set, the server will return an
|
227
|
+
# unroutable message with a Return method. If this flag is zero, the
|
228
|
+
# server silently drops the message.
|
229
|
+
#
|
230
|
+
# * :immediate => true | false (default false)
|
231
|
+
#
|
232
|
+
# This flag tells the server how to react if the message cannot be
|
233
|
+
# routed to a queue consumer immediately. If this flag is set, the
|
234
|
+
# server will return an undeliverable message with a Return method.
|
235
|
+
# If this flag is zero, the server will queue the message, but with
|
236
|
+
# no guarantee that it will ever be consumed.
|
237
|
+
#
|
238
|
+
# * :persistent
|
239
|
+
# True or False. When true, this message will remain in the queue until
|
240
|
+
# it is consumed (if the queue is durable). When false, the message is
|
241
|
+
# lost if the server restarts and the queue is recreated.
|
242
|
+
#
|
243
|
+
# For high-performance and low-latency, set :persistent => false so the
|
244
|
+
# message stays in memory and is never persisted to non-volatile (slow)
|
245
|
+
# storage.
|
246
|
+
#
|
247
|
+
def publish data, opts = {}
|
248
|
+
@mq.callback{
|
249
|
+
out = []
|
250
|
+
|
251
|
+
out << Protocol::Basic::Publish.new({ :exchange => name,
|
252
|
+
:routing_key => opts[:key] || @key }.merge(opts))
|
253
|
+
|
254
|
+
data = data.to_s
|
255
|
+
|
256
|
+
out << Protocol::Header.new(Protocol::Basic,
|
257
|
+
data.bytesize, { :content_type => 'application/octet-stream',
|
258
|
+
:delivery_mode => (opts[:persistent] ? 2 : 1),
|
259
|
+
:priority => 0 }.merge(opts))
|
260
|
+
|
261
|
+
out << Frame::Body.new(data)
|
262
|
+
|
263
|
+
@mq.send *out
|
264
|
+
}
|
265
|
+
self
|
266
|
+
end
|
267
|
+
|
268
|
+
# This method deletes an exchange. When an exchange is deleted all queue
|
269
|
+
# bindings on the exchange are cancelled.
|
270
|
+
#
|
271
|
+
# Further attempts to publish messages to a deleted exchange will raise
|
272
|
+
# an MQ::Error due to a channel close exception.
|
273
|
+
#
|
274
|
+
# exchange = MQ.direct('name', :key => 'foo.bar')
|
275
|
+
# exchange.delete
|
276
|
+
#
|
277
|
+
# == Options
|
278
|
+
# * :nowait => true | false (default true)
|
279
|
+
# If set, the server will not respond to the method. The client should
|
280
|
+
# not wait for a reply method. If the server could not complete the
|
281
|
+
# method it will raise a channel or connection exception.
|
282
|
+
#
|
283
|
+
# exchange.delete(:nowait => false)
|
284
|
+
#
|
285
|
+
# * :if_unused => true | false (default false)
|
286
|
+
# If set, the server will only delete the exchange if it has no queue
|
287
|
+
# bindings. If the exchange has queue bindings the server does not
|
288
|
+
# delete it but raises a channel exception instead (MQ:Error).
|
289
|
+
#
|
290
|
+
def delete opts = {}
|
291
|
+
@mq.callback{
|
292
|
+
@mq.send Protocol::Exchange::Delete.new({ :exchange => name,
|
293
|
+
:nowait => true }.merge(opts))
|
294
|
+
@mq.exchanges.delete name
|
295
|
+
}
|
296
|
+
nil
|
297
|
+
end
|
298
|
+
|
299
|
+
def reset
|
300
|
+
@deferred_status = nil
|
301
|
+
initialize @mq, @type, @name, @opts
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
class MQ
|
2
|
+
class Header
|
3
|
+
include AMQP
|
4
|
+
|
5
|
+
def initialize(mq, header_obj)
|
6
|
+
@mq = mq
|
7
|
+
@header = header_obj
|
8
|
+
end
|
9
|
+
|
10
|
+
# Acknowledges the receipt of this message with the server.
|
11
|
+
def ack
|
12
|
+
@mq.callback{
|
13
|
+
@mq.send Protocol::Basic::Ack.new(:delivery_tag => properties[:delivery_tag])
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
# Reject this message (XXX currently unimplemented in rabbitmq)
|
18
|
+
# * :requeue => true | false (default false)
|
19
|
+
def reject opts = {}
|
20
|
+
@mq.callback{
|
21
|
+
@mq.send Protocol::Basic::Reject.new(opts.merge(:delivery_tag => properties[:delivery_tag]))
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def method_missing meth, *args, &blk
|
26
|
+
@header.send meth, *args, &blk
|
27
|
+
end
|
28
|
+
|
29
|
+
def inspect
|
30
|
+
@header.inspect
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
class MQ
|
2
|
+
class Logger
|
3
|
+
def initialize *args, &block
|
4
|
+
opts = args.pop if args.last.is_a? Hash
|
5
|
+
opts ||= {}
|
6
|
+
|
7
|
+
printer(block) if block
|
8
|
+
|
9
|
+
@prop = opts
|
10
|
+
@tags = ([:timestamp] + args).uniq
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :prop
|
14
|
+
alias :base :prop
|
15
|
+
|
16
|
+
def log severity, *args
|
17
|
+
opts = args.pop if args.last.is_a? Hash and args.size != 1
|
18
|
+
opts ||= {}
|
19
|
+
opts = @prop.clone.update(opts)
|
20
|
+
|
21
|
+
data = args.shift
|
22
|
+
|
23
|
+
data = {:type => :exception,
|
24
|
+
:name => data.class.to_s.intern,
|
25
|
+
:backtrace => data.backtrace,
|
26
|
+
:message => data.message} if data.is_a? Exception
|
27
|
+
|
28
|
+
(@tags + args).each do |tag|
|
29
|
+
tag = tag.to_sym
|
30
|
+
case tag
|
31
|
+
when :timestamp
|
32
|
+
opts.update :timestamp => Time.now
|
33
|
+
when :hostname
|
34
|
+
@hostname ||= { :hostname => `hostname`.strip }
|
35
|
+
opts.update @hostname
|
36
|
+
when :process
|
37
|
+
@process_id ||= { :process_id => Process.pid,
|
38
|
+
:process_name => $0,
|
39
|
+
:process_parent_id => Process.ppid,
|
40
|
+
:thread_id => Thread.current.object_id }
|
41
|
+
opts.update :process => @process_id
|
42
|
+
else
|
43
|
+
(opts[:tags] ||= []) << tag
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
opts.update(:severity => severity,
|
48
|
+
:msg => data)
|
49
|
+
|
50
|
+
print(opts)
|
51
|
+
unless Logger.disabled?
|
52
|
+
MQ.fanout('logging', :durable => true).publish Marshal.dump(opts)
|
53
|
+
end
|
54
|
+
|
55
|
+
opts
|
56
|
+
end
|
57
|
+
alias :method_missing :log
|
58
|
+
|
59
|
+
def print data = nil, &block
|
60
|
+
if block
|
61
|
+
@printer = block
|
62
|
+
elsif data.is_a? Proc
|
63
|
+
@printer = data
|
64
|
+
elsif data
|
65
|
+
(pr = @printer || self.class.printer) and pr.call(data)
|
66
|
+
else
|
67
|
+
@printer
|
68
|
+
end
|
69
|
+
end
|
70
|
+
alias :printer :print
|
71
|
+
|
72
|
+
def self.printer &block
|
73
|
+
@printer = block if block
|
74
|
+
@printer
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.disabled?
|
78
|
+
!!@disabled
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.enable
|
82
|
+
@disabled = false
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.disable
|
86
|
+
@disabled = true
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,456 @@
|
|
1
|
+
class MQ
|
2
|
+
class Queue
|
3
|
+
include AMQP
|
4
|
+
|
5
|
+
# Queues store and forward messages. Queues can be configured in the server
|
6
|
+
# or created at runtime. Queues must be attached to at least one exchange
|
7
|
+
# in order to receive messages from publishers.
|
8
|
+
#
|
9
|
+
# Like an Exchange, queue names starting with 'amq.' are reserved for
|
10
|
+
# internal use. Attempts to create queue names in violation of this
|
11
|
+
# reservation will raise MQ:Error (ACCESS_REFUSED).
|
12
|
+
#
|
13
|
+
# When a queue is created without a name, the server will generate a
|
14
|
+
# unique name internally (not currently supported in this library).
|
15
|
+
#
|
16
|
+
# == Options
|
17
|
+
# * :passive => true | false (default false)
|
18
|
+
# If set, the server will not create the exchange if it does not
|
19
|
+
# already exist. The client can use this to check whether an exchange
|
20
|
+
# exists without modifying the server state.
|
21
|
+
#
|
22
|
+
# * :durable => true | false (default false)
|
23
|
+
# If set when creating a new queue, the queue will be marked as
|
24
|
+
# durable. Durable queues remain active when a server restarts.
|
25
|
+
# Non-durable queues (transient queues) are purged if/when a
|
26
|
+
# server restarts. Note that durable queues do not necessarily
|
27
|
+
# hold persistent messages, although it does not make sense to
|
28
|
+
# send persistent messages to a transient queue (though it is
|
29
|
+
# allowed).
|
30
|
+
#
|
31
|
+
# If the queue has already been declared, any redeclaration will
|
32
|
+
# ignore this setting. A queue may only be declared durable the
|
33
|
+
# first time when it is created.
|
34
|
+
#
|
35
|
+
# * :exclusive => true | false (default false)
|
36
|
+
# Exclusive queues may only be consumed from by the current connection.
|
37
|
+
# Setting the 'exclusive' flag always implies 'auto-delete'. Only a
|
38
|
+
# single consumer is allowed to remove messages from this queue.
|
39
|
+
#
|
40
|
+
# The default is a shared queue. Multiple clients may consume messages
|
41
|
+
# from this queue.
|
42
|
+
#
|
43
|
+
# Attempting to redeclare an already-declared queue as :exclusive => true
|
44
|
+
# will raise MQ:Error.
|
45
|
+
#
|
46
|
+
# * :auto_delete = true | false (default false)
|
47
|
+
# If set, the queue is deleted when all consumers have finished
|
48
|
+
# using it. Last consumer can be cancelled either explicitly or because
|
49
|
+
# its channel is closed. If there was no consumer ever on the queue, it
|
50
|
+
# won't be deleted.
|
51
|
+
#
|
52
|
+
# The server waits for a short period of time before
|
53
|
+
# determining the queue is unused to give time to the client code
|
54
|
+
# to bind an exchange to it.
|
55
|
+
#
|
56
|
+
# If the queue has been previously declared, this option is ignored
|
57
|
+
# on subsequent declarations.
|
58
|
+
#
|
59
|
+
# * :nowait => true | false (default true)
|
60
|
+
# If set, the server will not respond to the method. The client should
|
61
|
+
# not wait for a reply method. If the server could not complete the
|
62
|
+
# method it will raise a channel or connection exception.
|
63
|
+
#
|
64
|
+
def initialize mq, name, opts = {}
|
65
|
+
@mq = mq
|
66
|
+
@opts = opts
|
67
|
+
@bindings ||= {}
|
68
|
+
@mq.queues[@name = name] ||= self
|
69
|
+
unless opts[:no_declare]
|
70
|
+
@mq.callback{
|
71
|
+
@mq.send Protocol::Queue::Declare.new({ :queue => name,
|
72
|
+
:nowait => true }.merge(opts))
|
73
|
+
}
|
74
|
+
end
|
75
|
+
end
|
76
|
+
attr_reader :name
|
77
|
+
|
78
|
+
# This method binds a queue to an exchange. Until a queue is
|
79
|
+
# bound it will not receive any messages. In a classic messaging
|
80
|
+
# model, store-and-forward queues are bound to a dest exchange
|
81
|
+
# and subscription queues are bound to a dest_wild exchange.
|
82
|
+
#
|
83
|
+
# A valid exchange name (or reference) must be passed as the first
|
84
|
+
# parameter. Both of these are valid:
|
85
|
+
# exch = MQ.direct('foo exchange')
|
86
|
+
# queue = MQ.queue('bar queue')
|
87
|
+
# queue.bind('foo.exchange') # OR
|
88
|
+
# queue.bind(exch)
|
89
|
+
#
|
90
|
+
# It is not valid to call #bind without the +exchange+ parameter.
|
91
|
+
#
|
92
|
+
# It is unnecessary to call #bind when the exchange name and queue
|
93
|
+
# name match exactly (for +direct+ and +fanout+ exchanges only).
|
94
|
+
# There is an implicit bind which will deliver the messages from
|
95
|
+
# the exchange to the queue.
|
96
|
+
#
|
97
|
+
# == Options
|
98
|
+
# * :key => 'some string'
|
99
|
+
# Specifies the routing key for the binding. The routing key is
|
100
|
+
# used for routing messages depending on the exchange configuration.
|
101
|
+
# Not all exchanges use a routing key - refer to the specific
|
102
|
+
# exchange documentation. If the routing key is empty and the queue
|
103
|
+
# name is empty, the routing key will be the current queue for the
|
104
|
+
# channel, which is the last declared queue.
|
105
|
+
#
|
106
|
+
# * :nowait => true | false (default true)
|
107
|
+
# If set, the server will not respond to the method. The client should
|
108
|
+
# not wait for a reply method. If the server could not complete the
|
109
|
+
# method it will raise a channel or connection exception.
|
110
|
+
#
|
111
|
+
def bind exchange, opts = {}
|
112
|
+
exchange = exchange.respond_to?(:name) ? exchange.name : exchange
|
113
|
+
@bindings[exchange] = opts
|
114
|
+
|
115
|
+
@mq.callback{
|
116
|
+
@mq.send Protocol::Queue::Bind.new({ :queue => name,
|
117
|
+
:exchange => exchange,
|
118
|
+
:routing_key => opts[:key],
|
119
|
+
:nowait => true }.merge(opts))
|
120
|
+
}
|
121
|
+
self
|
122
|
+
end
|
123
|
+
|
124
|
+
# Remove the binding between the queue and exchange. The queue will
|
125
|
+
# not receive any more messages until it is bound to another
|
126
|
+
# exchange.
|
127
|
+
#
|
128
|
+
# Due to the asynchronous nature of the protocol, it is possible for
|
129
|
+
# "in flight" messages to be received after this call completes.
|
130
|
+
# Those messages will be serviced by the last block used in a
|
131
|
+
# #subscribe or #pop call.
|
132
|
+
#
|
133
|
+
# * :nowait => true | false (default true)
|
134
|
+
# If set, the server will not respond to the method. The client should
|
135
|
+
# not wait for a reply method. If the server could not complete the
|
136
|
+
# method it will raise a channel or connection exception.
|
137
|
+
#
|
138
|
+
def unbind exchange, opts = {}
|
139
|
+
exchange = exchange.respond_to?(:name) ? exchange.name : exchange
|
140
|
+
@bindings.delete exchange
|
141
|
+
|
142
|
+
@mq.callback{
|
143
|
+
@mq.send Protocol::Queue::Unbind.new({ :queue => name,
|
144
|
+
:exchange => exchange,
|
145
|
+
:routing_key => opts[:key],
|
146
|
+
:nowait => true }.merge(opts))
|
147
|
+
}
|
148
|
+
self
|
149
|
+
end
|
150
|
+
|
151
|
+
# This method deletes a queue. When a queue is deleted any pending
|
152
|
+
# messages are sent to a dead-letter queue if this is defined in the
|
153
|
+
# server configuration, and all consumers on the queue are cancelled.
|
154
|
+
#
|
155
|
+
# == Options
|
156
|
+
# * :if_unused => true | false (default false)
|
157
|
+
# If set, the server will only delete the queue if it has no
|
158
|
+
# consumers. If the queue has consumers the server does does not
|
159
|
+
# delete it but raises a channel exception instead.
|
160
|
+
#
|
161
|
+
# * :if_empty => true | false (default false)
|
162
|
+
# If set, the server will only delete the queue if it has no
|
163
|
+
# messages. If the queue is not empty the server raises a channel
|
164
|
+
# exception.
|
165
|
+
#
|
166
|
+
# * :nowait => true | false (default true)
|
167
|
+
# If set, the server will not respond to the method. The client should
|
168
|
+
# not wait for a reply method. If the server could not complete the
|
169
|
+
# method it will raise a channel or connection exception.
|
170
|
+
#
|
171
|
+
def delete opts = {}
|
172
|
+
@mq.callback{
|
173
|
+
@mq.send Protocol::Queue::Delete.new({ :queue => name,
|
174
|
+
:nowait => true }.merge(opts))
|
175
|
+
}
|
176
|
+
@mq.queues.delete @name
|
177
|
+
nil
|
178
|
+
end
|
179
|
+
|
180
|
+
# Purge all messages from the queue.
|
181
|
+
#
|
182
|
+
def purge opts = {}
|
183
|
+
@mq.callback{
|
184
|
+
@mq.send Protocol::Queue::Purge.new({ :queue => name,
|
185
|
+
:nowait => true }.merge(opts))
|
186
|
+
}
|
187
|
+
nil
|
188
|
+
end
|
189
|
+
|
190
|
+
# This method provides a direct access to the messages in a queue
|
191
|
+
# using a synchronous dialogue that is designed for specific types of
|
192
|
+
# application where synchronous functionality is more important than
|
193
|
+
# performance.
|
194
|
+
#
|
195
|
+
# The provided block is passed a single message each time pop is called.
|
196
|
+
#
|
197
|
+
# EM.run do
|
198
|
+
# exchange = MQ.direct("foo queue")
|
199
|
+
# EM.add_periodic_timer(1) do
|
200
|
+
# exchange.publish("random number #{rand(1000)}")
|
201
|
+
# end
|
202
|
+
#
|
203
|
+
# # note that #bind is never called; it is implicit because
|
204
|
+
# # the exchange and queue names match
|
205
|
+
# queue = MQ.queue('foo queue')
|
206
|
+
# queue.pop { |body| puts "received payload [#{body}]" }
|
207
|
+
#
|
208
|
+
# EM.add_periodic_timer(1) { queue.pop }
|
209
|
+
# end
|
210
|
+
#
|
211
|
+
# If the block takes 2 parameters, both the +header+ and the +body+ will
|
212
|
+
# be passed in for processing. The header object is defined by
|
213
|
+
# AMQP::Protocol::Header.
|
214
|
+
#
|
215
|
+
# EM.run do
|
216
|
+
# exchange = MQ.direct("foo queue")
|
217
|
+
# EM.add_periodic_timer(1) do
|
218
|
+
# exchange.publish("random number #{rand(1000)}")
|
219
|
+
# end
|
220
|
+
#
|
221
|
+
# queue = MQ.queue('foo queue')
|
222
|
+
# queue.pop do |header, body|
|
223
|
+
# p header
|
224
|
+
# puts "received payload [#{body}]"
|
225
|
+
# end
|
226
|
+
#
|
227
|
+
# EM.add_periodic_timer(1) { queue.pop }
|
228
|
+
# end
|
229
|
+
#
|
230
|
+
# == Options
|
231
|
+
# * :ack => true | false (default false)
|
232
|
+
# If this field is set to false the server does not expect acknowledgments
|
233
|
+
# for messages. That is, when a message is delivered to the client
|
234
|
+
# the server automatically and silently acknowledges it on behalf
|
235
|
+
# of the client. This functionality increases performance but at
|
236
|
+
# the cost of reliability. Messages can get lost if a client dies
|
237
|
+
# before it can deliver them to the application.
|
238
|
+
#
|
239
|
+
# * :nowait => true | false (default true)
|
240
|
+
# If set, the server will not respond to the method. The client should
|
241
|
+
# not wait for a reply method. If the server could not complete the
|
242
|
+
# method it will raise a channel or connection exception.
|
243
|
+
#
|
244
|
+
def pop opts = {}, &blk
|
245
|
+
if blk
|
246
|
+
@on_pop = blk
|
247
|
+
@on_pop_opts = opts
|
248
|
+
end
|
249
|
+
|
250
|
+
@mq.callback{
|
251
|
+
@mq.get_queue{ |q|
|
252
|
+
q.push(self)
|
253
|
+
@mq.send Protocol::Basic::Get.new({ :queue => name,
|
254
|
+
:consumer_tag => name,
|
255
|
+
:no_ack => !opts[:ack],
|
256
|
+
:nowait => true }.merge(opts))
|
257
|
+
}
|
258
|
+
}
|
259
|
+
|
260
|
+
self
|
261
|
+
end
|
262
|
+
|
263
|
+
# Subscribes to asynchronous message delivery.
|
264
|
+
#
|
265
|
+
# The provided block is passed a single message each time the
|
266
|
+
# exchange matches a message to this queue.
|
267
|
+
#
|
268
|
+
# EM.run do
|
269
|
+
# exchange = MQ.direct("foo queue")
|
270
|
+
# EM.add_periodic_timer(1) do
|
271
|
+
# exchange.publish("random number #{rand(1000)}")
|
272
|
+
# end
|
273
|
+
#
|
274
|
+
# queue = MQ.queue('foo queue')
|
275
|
+
# queue.subscribe { |body| puts "received payload [#{body}]" }
|
276
|
+
# end
|
277
|
+
#
|
278
|
+
# If the block takes 2 parameters, both the +header+ and the +body+ will
|
279
|
+
# be passed in for processing. The header object is defined by
|
280
|
+
# AMQP::Protocol::Header.
|
281
|
+
#
|
282
|
+
# EM.run do
|
283
|
+
# exchange = MQ.direct("foo queue")
|
284
|
+
# EM.add_periodic_timer(1) do
|
285
|
+
# exchange.publish("random number #{rand(1000)}")
|
286
|
+
# end
|
287
|
+
#
|
288
|
+
# # note that #bind is never called; it is implicit because
|
289
|
+
# # the exchange and queue names match
|
290
|
+
# queue = MQ.queue('foo queue')
|
291
|
+
# queue.subscribe do |header, body|
|
292
|
+
# p header
|
293
|
+
# puts "received payload [#{body}]"
|
294
|
+
# end
|
295
|
+
# end
|
296
|
+
#
|
297
|
+
# == Options
|
298
|
+
# * :ack => true | false (default false)
|
299
|
+
# If this field is set to false the server does not expect acknowledgments
|
300
|
+
# for messages. That is, when a message is delivered to the client
|
301
|
+
# the server automatically and silently acknowledges it on behalf
|
302
|
+
# of the client. This functionality increases performance but at
|
303
|
+
# the cost of reliability. Messages can get lost if a client dies
|
304
|
+
# before it can deliver them to the application.
|
305
|
+
#
|
306
|
+
# * :nowait => true | false (default true)
|
307
|
+
# If set, the server will not respond to the method. The client should
|
308
|
+
# not wait for a reply method. If the server could not complete the
|
309
|
+
# method it will raise a channel or connection exception.
|
310
|
+
#
|
311
|
+
# * :confirm => proc (default nil)
|
312
|
+
# If set, this proc will be called when the server confirms subscription
|
313
|
+
# to the queue with a ConsumeOk message. Setting this option will
|
314
|
+
# automatically set :nowait => false. This is required for the server
|
315
|
+
# to send a confirmation.
|
316
|
+
#
|
317
|
+
def subscribe opts = {}, &blk
|
318
|
+
@consumer_tag = "#{name}-#{Kernel.rand(999_999_999_999)}"
|
319
|
+
@mq.consumers[@consumer_tag] = self
|
320
|
+
|
321
|
+
raise Error, 'already subscribed to the queue' if subscribed?
|
322
|
+
|
323
|
+
@on_msg = blk
|
324
|
+
@on_msg_opts = opts
|
325
|
+
opts[:nowait] = false if (@on_confirm_subscribe = opts[:confirm])
|
326
|
+
|
327
|
+
@mq.callback{
|
328
|
+
@mq.send Protocol::Basic::Consume.new({ :queue => name,
|
329
|
+
:consumer_tag => @consumer_tag,
|
330
|
+
:no_ack => !opts[:ack],
|
331
|
+
:nowait => true }.merge(opts))
|
332
|
+
}
|
333
|
+
self
|
334
|
+
end
|
335
|
+
|
336
|
+
# Removes the subscription from the queue and cancels the consumer.
|
337
|
+
# New messages will not be received by the queue. This call is similar
|
338
|
+
# in result to calling #unbind.
|
339
|
+
#
|
340
|
+
# Due to the asynchronous nature of the protocol, it is possible for
|
341
|
+
# "in flight" messages to be received after this call completes.
|
342
|
+
# Those messages will be serviced by the last block used in a
|
343
|
+
# #subscribe or #pop call.
|
344
|
+
#
|
345
|
+
# Additionally, if the queue was created with _autodelete_ set to
|
346
|
+
# true, the server will delete the queue after its wait period
|
347
|
+
# has expired unless the queue is bound to an active exchange.
|
348
|
+
#
|
349
|
+
# The method accepts a block which will be executed when the
|
350
|
+
# unsubscription request is acknowledged as complete by the server.
|
351
|
+
#
|
352
|
+
# * :nowait => true | false (default true)
|
353
|
+
# If set, the server will not respond to the method. The client should
|
354
|
+
# not wait for a reply method. If the server could not complete the
|
355
|
+
# method it will raise a channel or connection exception.
|
356
|
+
#
|
357
|
+
def unsubscribe opts = {}, &blk
|
358
|
+
@on_cancel = blk
|
359
|
+
@mq.callback{
|
360
|
+
@mq.send Protocol::Basic::Cancel.new({ :consumer_tag => @consumer_tag }.merge(opts))
|
361
|
+
}
|
362
|
+
self
|
363
|
+
end
|
364
|
+
|
365
|
+
def publish data, opts = {}
|
366
|
+
exchange.publish(data, opts)
|
367
|
+
end
|
368
|
+
|
369
|
+
# Boolean check to see if the current queue has already been subscribed
|
370
|
+
# to an exchange.
|
371
|
+
#
|
372
|
+
# Attempts to #subscribe multiple times to any exchange will raise an
|
373
|
+
# Exception. Only a single block at a time can be associated with any
|
374
|
+
# one queue for processing incoming messages.
|
375
|
+
#
|
376
|
+
def subscribed?
|
377
|
+
!!@on_msg
|
378
|
+
end
|
379
|
+
|
380
|
+
# Passes the message to the block passed to pop or subscribe.
|
381
|
+
#
|
382
|
+
# Performs an arity check on the block's parameters. If arity == 1,
|
383
|
+
# pass only the message body. If arity != 1, pass the headers and
|
384
|
+
# the body to the block.
|
385
|
+
#
|
386
|
+
# See AMQP::Protocol::Header for the hash properties available from
|
387
|
+
# the headers parameter. See #pop or #subscribe for a code example.
|
388
|
+
#
|
389
|
+
def receive headers, body
|
390
|
+
headers = MQ::Header.new(@mq, headers)
|
391
|
+
|
392
|
+
if cb = (@on_msg || @on_pop)
|
393
|
+
cb.call *(cb.arity == 1 ? [body] : [headers, body])
|
394
|
+
end
|
395
|
+
end
|
396
|
+
|
397
|
+
# Get the number of messages and consumers on a queue.
|
398
|
+
#
|
399
|
+
# MQ.queue('name').status{ |num_messages, num_consumers|
|
400
|
+
# puts num_messages
|
401
|
+
# }
|
402
|
+
#
|
403
|
+
def status opts = {}, &blk
|
404
|
+
@on_status = blk
|
405
|
+
@mq.callback{
|
406
|
+
@mq.send Protocol::Queue::Declare.new({ :queue => name,
|
407
|
+
:passive => true }.merge(opts))
|
408
|
+
}
|
409
|
+
self
|
410
|
+
end
|
411
|
+
|
412
|
+
def receive_status declare_ok
|
413
|
+
if @on_status
|
414
|
+
m, c = declare_ok.message_count, declare_ok.consumer_count
|
415
|
+
@on_status.call *(@on_status.arity == 1 ? [m] : [m, c])
|
416
|
+
@on_status = nil
|
417
|
+
end
|
418
|
+
end
|
419
|
+
|
420
|
+
def confirm_subscribe
|
421
|
+
@on_confirm_subscribe.call if @on_confirm_subscribe
|
422
|
+
@on_confirm_subscribe = nil
|
423
|
+
end
|
424
|
+
|
425
|
+
def cancelled
|
426
|
+
@on_cancel.call if @on_cancel
|
427
|
+
@on_cancel = @on_msg = nil
|
428
|
+
@mq.consumers.delete @consumer_tag
|
429
|
+
@consumer_tag = nil
|
430
|
+
end
|
431
|
+
|
432
|
+
def reset
|
433
|
+
@deferred_status = nil
|
434
|
+
initialize @mq, @name, @opts
|
435
|
+
|
436
|
+
binds = @bindings
|
437
|
+
@bindings = {}
|
438
|
+
binds.each{|ex,opts| bind(ex, opts) }
|
439
|
+
|
440
|
+
if blk = @on_msg
|
441
|
+
@on_msg = nil
|
442
|
+
subscribe @on_msg_opts, &blk
|
443
|
+
end
|
444
|
+
|
445
|
+
if @on_pop
|
446
|
+
pop @on_pop_opts, &@on_pop
|
447
|
+
end
|
448
|
+
end
|
449
|
+
|
450
|
+
private
|
451
|
+
|
452
|
+
def exchange
|
453
|
+
@exchange ||= Exchange.new(@mq, :direct, '', :key => name)
|
454
|
+
end
|
455
|
+
end
|
456
|
+
end
|