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,69 @@
|
|
1
|
+
EMFORK = $0 == __FILE__
|
2
|
+
|
3
|
+
if EMFORK
|
4
|
+
require 'rubygems'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'eventmachine'
|
8
|
+
|
9
|
+
#:stopdoc:
|
10
|
+
|
11
|
+
# helper to fork off EM reactors
|
12
|
+
def EM.fork num = 1, &blk
|
13
|
+
unless @forks
|
14
|
+
trap('CHLD'){
|
15
|
+
pid = Process.wait
|
16
|
+
p [:pid, pid, :died] if EMFORK
|
17
|
+
block = @forks.delete(pid)
|
18
|
+
EM.fork(1, &block)
|
19
|
+
}
|
20
|
+
|
21
|
+
trap('EXIT'){
|
22
|
+
p [:pid, Process.pid, :exit] if EMFORK
|
23
|
+
@forks.keys.each{ |pid|
|
24
|
+
p [:pid, Process.pid, :killing, pid] if EMFORK
|
25
|
+
Process.kill('USR1', pid)
|
26
|
+
}
|
27
|
+
}
|
28
|
+
|
29
|
+
@forks = {}
|
30
|
+
end
|
31
|
+
|
32
|
+
num.times do
|
33
|
+
pid = EM.fork_reactor do
|
34
|
+
p [:pid, Process.pid, :started] if EMFORK
|
35
|
+
|
36
|
+
trap('USR1'){ EM.stop_event_loop }
|
37
|
+
trap('CHLD'){}
|
38
|
+
trap('EXIT'){}
|
39
|
+
|
40
|
+
blk.call
|
41
|
+
end
|
42
|
+
|
43
|
+
@forks[pid] = blk
|
44
|
+
p [:children, EM.forks] if EMFORK
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def EM.forks
|
49
|
+
@forks ? @forks.keys : []
|
50
|
+
end
|
51
|
+
|
52
|
+
if EMFORK
|
53
|
+
p 'starting reactor'
|
54
|
+
|
55
|
+
trap('INT'){ EM.stop_event_loop }
|
56
|
+
|
57
|
+
EM.run{
|
58
|
+
p [:parent, Process.pid]
|
59
|
+
|
60
|
+
EM.fork(2){
|
61
|
+
EM.add_periodic_timer(1) do
|
62
|
+
p [:fork, Process.pid, :ping]
|
63
|
+
end
|
64
|
+
}
|
65
|
+
|
66
|
+
}
|
67
|
+
|
68
|
+
p 'reactor stopped'
|
69
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2012 RightScale Inc
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# "Software"), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
|
23
|
+
Dir[File.expand_path('../ha_client/*.rb', __FILE__)].each do |filename|
|
24
|
+
require filename
|
25
|
+
end
|
@@ -0,0 +1,690 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2009-2012 RightScale Inc
|
3
|
+
#
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
5
|
+
# a copy of this software and associated documentation files (the
|
6
|
+
# "Software"), to deal in the Software without restriction, including
|
7
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
8
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
9
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
10
|
+
# the following conditions:
|
11
|
+
#
|
12
|
+
# The above copyright notice and this permission notice shall be
|
13
|
+
# included in all copies or substantial portions of the Software.
|
14
|
+
#
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
17
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
19
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
20
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
|
+
|
23
|
+
module RightAMQP
|
24
|
+
|
25
|
+
# Client for accessing AMQP broker
|
26
|
+
class BrokerClient
|
27
|
+
|
28
|
+
include RightSupport::Log::Mixin
|
29
|
+
|
30
|
+
# Set of possible broker connection status values
|
31
|
+
STATUS = [
|
32
|
+
:connecting, # Initiated AMQP connection but not yet confirmed that connected
|
33
|
+
:connected, # Confirmed AMQP connection
|
34
|
+
:stopping, # Broker is stopping service and, although still connected, is no longer usable
|
35
|
+
:disconnected, # Notified by AMQP that connection has been lost and attempting to reconnect
|
36
|
+
:closed, # AMQP connection closed explicitly or because of too many failed connect attempts
|
37
|
+
:failed # Failed to connect due to internal failure or AMQP failure to connect
|
38
|
+
]
|
39
|
+
|
40
|
+
# (AMQP::Channel) Channel of AMQP connection used by this client
|
41
|
+
attr_reader :channel
|
42
|
+
|
43
|
+
# (String) Broker identity
|
44
|
+
attr_reader :identity
|
45
|
+
|
46
|
+
# (String) Broker alias, used in logs
|
47
|
+
attr_reader :alias
|
48
|
+
|
49
|
+
# (String) Host name
|
50
|
+
attr_reader :host
|
51
|
+
|
52
|
+
# (Integer) Port number
|
53
|
+
attr_reader :port
|
54
|
+
|
55
|
+
# (Integer) Unique index for broker within given set, used in alias
|
56
|
+
attr_reader :index
|
57
|
+
|
58
|
+
# (Symbol) AMQP connection STATUS value
|
59
|
+
attr_reader :status
|
60
|
+
|
61
|
+
# (Array) List of MQ::Queue queues currently subscribed
|
62
|
+
attr_reader :queues
|
63
|
+
|
64
|
+
# (Boolean) Whether last connect attempt failed
|
65
|
+
attr_reader :last_failed
|
66
|
+
|
67
|
+
# (RightSupport::Stats::Activity) AMQP lost connection statistics
|
68
|
+
attr_reader :disconnects
|
69
|
+
|
70
|
+
# (RightSupport::Stats::Activity) AMQP connection failure statistics
|
71
|
+
attr_reader :failures
|
72
|
+
|
73
|
+
# (Integer) Number of attempts to connect after failure
|
74
|
+
attr_reader :retries
|
75
|
+
|
76
|
+
# Create broker client
|
77
|
+
#
|
78
|
+
# === Parameters
|
79
|
+
# identity(String):: Broker identity
|
80
|
+
# address(Hash):: Broker address
|
81
|
+
# :host(String:: IP host name or address
|
82
|
+
# :port(Integer):: TCP port number for individual broker
|
83
|
+
# :index(String):: Unique index for broker within set of brokers for use in forming alias
|
84
|
+
# serializer(Serializer):: Serializer used for unmarshaling received messages to packets
|
85
|
+
# (responds to :load); if nil, has same effect as setting subscribe option :no_unserialize
|
86
|
+
# exceptions(RightSupport::Stats::Exceptions):: Exception statistics container
|
87
|
+
# options(Hash):: Configuration options
|
88
|
+
# :user(String):: User name
|
89
|
+
# :pass(String):: Password
|
90
|
+
# :vhost(String):: Virtual host path name
|
91
|
+
# :insist(Boolean):: Whether to suppress redirection of connection
|
92
|
+
# :reconnect_interval(Integer):: Number of seconds between reconnect attempts
|
93
|
+
# :heartbeat(Integer):: Number of seconds between AMQP connection heartbeats used to keep
|
94
|
+
# connection alive, e.g., when AMQP broker is behind a firewall
|
95
|
+
# :prefetch(Integer):: Maximum number of messages the AMQP broker is to prefetch for the agent
|
96
|
+
# before it receives an ack. Value 1 ensures that only last unacknowledged gets redelivered
|
97
|
+
# if the agent crashes. Value 0 means unlimited prefetch.
|
98
|
+
# :exception_on_receive_callback(Proc):: Callback activated on a receive exception with parameters
|
99
|
+
# message(Object):: Message received
|
100
|
+
# exception(Exception):: Exception raised
|
101
|
+
# :update_status_callback(Proc):: Callback activated on a connection status change with parameters
|
102
|
+
# broker(BrokerClient):: Broker client
|
103
|
+
# connected_before(Boolean):: Whether was connected prior to this status change
|
104
|
+
# existing(BrokerClient|nil):: Existing broker client for this address, or nil if none
|
105
|
+
#
|
106
|
+
# === Raise
|
107
|
+
# ArgumentError:: If serializer does not respond to :dump and :load
|
108
|
+
def initialize(identity, address, serializer, exceptions, options, existing = nil)
|
109
|
+
@options = options
|
110
|
+
@identity = identity
|
111
|
+
@host = address[:host]
|
112
|
+
@port = address[:port].to_i
|
113
|
+
@index = address[:index].to_i
|
114
|
+
@alias = "b#{@index}"
|
115
|
+
unless serializer.nil? || [:dump, :load].all? { |m| serializer.respond_to?(m) }
|
116
|
+
raise ArgumentError, "serializer must be a class/object that responds to :dump and :load"
|
117
|
+
end
|
118
|
+
@serializer = serializer
|
119
|
+
@exceptions = exceptions
|
120
|
+
@queues = []
|
121
|
+
@last_failed = false
|
122
|
+
@disconnects = RightSupport::Stats::Activity.new(measure_rate = false)
|
123
|
+
@failures = RightSupport::Stats::Activity.new(measure_rate = false)
|
124
|
+
@retries = 0
|
125
|
+
|
126
|
+
connect(address, @options[:reconnect_interval])
|
127
|
+
|
128
|
+
if existing
|
129
|
+
@disconnects = existing.disconnects
|
130
|
+
@failures = existing.failures
|
131
|
+
@last_failed = existing.last_failed
|
132
|
+
@retries = existing.retries
|
133
|
+
update_failure if @status == :failed
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Determine whether the broker connection is usable, i.e., connecting or confirmed connected
|
138
|
+
#
|
139
|
+
# === Return
|
140
|
+
# (Boolean):: true if usable, otherwise false
|
141
|
+
def usable?
|
142
|
+
[:connected, :connecting].include?(@status)
|
143
|
+
end
|
144
|
+
|
145
|
+
# Determine whether this client is currently connected to the broker
|
146
|
+
#
|
147
|
+
# === Return
|
148
|
+
# (Boolean):: true if connected, otherwise false
|
149
|
+
def connected?
|
150
|
+
@status == :connected
|
151
|
+
end
|
152
|
+
|
153
|
+
# Determine whether the broker connection has failed
|
154
|
+
#
|
155
|
+
# === Return
|
156
|
+
# (Boolean):: true if failed, otherwise false
|
157
|
+
def failed?(backoff = false)
|
158
|
+
@status == :failed
|
159
|
+
end
|
160
|
+
|
161
|
+
# Subscribe an AMQP queue to an AMQP exchange
|
162
|
+
# Do not wait for confirmation from broker that subscription is complete
|
163
|
+
# When a message is received, acknowledge, unserialize, and log it as specified
|
164
|
+
# If the message is unserialized and it is not of the right type, it is dropped after logging a warning
|
165
|
+
#
|
166
|
+
# === Parameters
|
167
|
+
# queue(Hash):: AMQP queue being subscribed with keys :name and :options,
|
168
|
+
# which are the standard AMQP ones plus
|
169
|
+
# :no_declare(Boolean):: Whether to skip declaring this queue on the broker
|
170
|
+
# to cause its creation; for use when client does not have permission to create or
|
171
|
+
# knows the queue already exists and wants to avoid declare overhead
|
172
|
+
# exchange(Hash|nil):: AMQP exchange to subscribe to with keys :type, :name, and :options,
|
173
|
+
# nil means use empty exchange by directly subscribing to queue; the :options are the
|
174
|
+
# standard AMQP ones plus
|
175
|
+
# :no_declare(Boolean):: Whether to skip declaring this exchange on the broker
|
176
|
+
# to cause its creation; for use when client does not have create permission or
|
177
|
+
# knows the exchange already exists and wants to avoid declare overhead
|
178
|
+
# options(Hash):: Subscribe options:
|
179
|
+
# :ack(Boolean):: Explicitly acknowledge received messages to AMQP
|
180
|
+
# :no_unserialize(Boolean):: Do not unserialize message, this is an escape for special
|
181
|
+
# situations like enrollment, also implicitly disables receive filtering and logging;
|
182
|
+
# this option is implicitly invoked if initialize without a serializer
|
183
|
+
# (packet class)(Array(Symbol)):: Filters to be applied in to_s when logging packet to :info,
|
184
|
+
# only packet classes specified are accepted, others are not processed but are logged with error
|
185
|
+
# :category(String):: Packet category description to be used in error messages
|
186
|
+
# :log_data(String):: Additional data to display at end of log entry
|
187
|
+
# :no_log(Boolean):: Disable receive logging unless debug level
|
188
|
+
# :exchange2(Hash):: Additional exchange to which same queue is to be bound
|
189
|
+
# :brokers(Array):: Identity of brokers for which to subscribe, defaults to all usable if nil or empty
|
190
|
+
#
|
191
|
+
# === Block
|
192
|
+
# Block with following parameters to be called each time exchange matches a message to the queue:
|
193
|
+
# identity(String):: Serialized identity of broker delivering the message
|
194
|
+
# message(Packet|String):: Message received, which is unserialized unless :no_unserialize was specified
|
195
|
+
# header(AMQP::Protocol::Header):: Message header (optional block parameter)
|
196
|
+
#
|
197
|
+
# === Return
|
198
|
+
# (Boolean):: true if subscribe successfully or if already subscribed, otherwise false
|
199
|
+
def subscribe(queue, exchange = nil, options = {}, &blk)
|
200
|
+
return false unless usable?
|
201
|
+
return true unless @queues.select { |q| q.name == queue[:name] }.empty?
|
202
|
+
|
203
|
+
to_exchange = if exchange
|
204
|
+
if options[:exchange2]
|
205
|
+
" to exchanges #{exchange[:name]} and #{options[:exchange2][:name]}"
|
206
|
+
else
|
207
|
+
" to exchange #{exchange[:name]}"
|
208
|
+
end
|
209
|
+
end
|
210
|
+
queue_options = queue[:options] || {}
|
211
|
+
exchange_options = (exchange && exchange[:options]) || {}
|
212
|
+
|
213
|
+
begin
|
214
|
+
logger.info("[setup] Subscribing queue #{queue[:name]}#{to_exchange} on broker #{@alias}")
|
215
|
+
q = @channel.queue(queue[:name], queue_options)
|
216
|
+
@queues << q
|
217
|
+
if exchange
|
218
|
+
x = @channel.__send__(exchange[:type], exchange[:name], exchange_options)
|
219
|
+
binding = q.bind(x, options[:key] ? {:key => options[:key]} : {})
|
220
|
+
if exchange2 = options[:exchange2]
|
221
|
+
q.bind(@channel.__send__(exchange2[:type], exchange2[:name], exchange2[:options] || {}))
|
222
|
+
end
|
223
|
+
q = binding
|
224
|
+
end
|
225
|
+
if options[:ack]
|
226
|
+
q.subscribe(:ack => true) do |header, message|
|
227
|
+
begin
|
228
|
+
# Ack now before processing to avoid risk of duplication after a crash
|
229
|
+
header.ack
|
230
|
+
if options[:no_unserialize] || @serializer.nil?
|
231
|
+
execute_callback(blk, @identity, message, header)
|
232
|
+
elsif message == "nil"
|
233
|
+
# This happens as part of connecting an instance agent to a broker prior to version 13
|
234
|
+
logger.debug("RECV #{@alias} nil message ignored")
|
235
|
+
elsif
|
236
|
+
packet = receive(queue[:name], message, options)
|
237
|
+
execute_callback(blk, @identity, packet, header) if packet
|
238
|
+
end
|
239
|
+
true
|
240
|
+
rescue Exception => e
|
241
|
+
logger.exception("Failed executing block for message from queue #{queue.inspect}#{to_exchange} " +
|
242
|
+
"on broker #{@alias}", e, :trace)
|
243
|
+
@exceptions.track("receive", e)
|
244
|
+
false
|
245
|
+
end
|
246
|
+
end
|
247
|
+
else
|
248
|
+
q.subscribe do |header, message|
|
249
|
+
begin
|
250
|
+
if options[:no_unserialize] || @serializer.nil?
|
251
|
+
execute_callback(blk, @identity, message, header)
|
252
|
+
elsif message == "nil"
|
253
|
+
# This happens as part of connecting an instance agent to a broker
|
254
|
+
logger.debug("RECV #{@alias} nil message ignored")
|
255
|
+
elsif
|
256
|
+
packet = receive(queue[:name], message, options)
|
257
|
+
execute_callback(blk, @identity, packet, header) if packet
|
258
|
+
end
|
259
|
+
true
|
260
|
+
rescue Exception => e
|
261
|
+
logger.exception("Failed executing block for message from queue #{queue.inspect}#{to_exchange} " +
|
262
|
+
"on broker #{@alias}", e, :trace)
|
263
|
+
@exceptions.track("receive", e)
|
264
|
+
false
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
rescue Exception => e
|
269
|
+
logger.exception("Failed subscribing queue #{queue.inspect}#{to_exchange} on broker #{@alias}", e, :trace)
|
270
|
+
@exceptions.track("subscribe", e)
|
271
|
+
false
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
# Unsubscribe from the specified queues
|
276
|
+
# Silently ignore unknown queues
|
277
|
+
#
|
278
|
+
# === Parameters
|
279
|
+
# queue_names(Array):: Names of queues previously subscribed to
|
280
|
+
#
|
281
|
+
# === Block
|
282
|
+
# Optional block to be called with no parameters when each unsubscribe completes
|
283
|
+
#
|
284
|
+
# === Return
|
285
|
+
# true:: Always return true
|
286
|
+
def unsubscribe(queue_names, &blk)
|
287
|
+
if usable?
|
288
|
+
@queues.each do |q|
|
289
|
+
if queue_names.include?(q.name)
|
290
|
+
begin
|
291
|
+
logger.info("[stop] Unsubscribing queue #{q.name} on broker #{@alias}")
|
292
|
+
q.unsubscribe { blk.call if blk }
|
293
|
+
rescue Exception => e
|
294
|
+
logger.exception("Failed unsubscribing queue #{q.name} on broker #{@alias}", e, :trace)
|
295
|
+
@exceptions.track("unsubscribe", e)
|
296
|
+
blk.call if blk
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
300
|
+
end
|
301
|
+
true
|
302
|
+
end
|
303
|
+
|
304
|
+
# Declare queue or exchange object but do not subscribe to it
|
305
|
+
#
|
306
|
+
# === Parameters
|
307
|
+
# type(Symbol):: Type of object: :queue, :direct, :fanout or :topic
|
308
|
+
# name(String):: Name of object
|
309
|
+
# options(Hash):: Standard AMQP declare options
|
310
|
+
#
|
311
|
+
# === Return
|
312
|
+
# (Boolean):: true if declare successfully, otherwise false
|
313
|
+
def declare(type, name, options = {})
|
314
|
+
return false unless usable?
|
315
|
+
begin
|
316
|
+
logger.info("[setup] Declaring #{name} #{type.to_s} on broker #{@alias}")
|
317
|
+
delete_amqp_resources(:queue, name)
|
318
|
+
@channel.__send__(type, name, options)
|
319
|
+
true
|
320
|
+
rescue Exception => e
|
321
|
+
logger.exception("Failed declaring #{type.to_s} #{name} on broker #{@alias}", e, :trace)
|
322
|
+
@exceptions.track("declare", e)
|
323
|
+
false
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
# Publish message to AMQP exchange
|
328
|
+
#
|
329
|
+
# === Parameters
|
330
|
+
# exchange(Hash):: AMQP exchange to subscribe to with keys :type, :name, and :options,
|
331
|
+
# which are the standard AMQP ones plus
|
332
|
+
# :no_declare(Boolean):: Whether to skip declaring this exchange or queue on the broker
|
333
|
+
# to cause its creation; for use when client does not have create permission or
|
334
|
+
# knows the object already exists and wants to avoid declare overhead
|
335
|
+
# :declare(Boolean):: Whether to delete this exchange or queue from the AMQP cache
|
336
|
+
# to force it to be declared on the broker and thus be created if it does not exist
|
337
|
+
# packet(Packet):: Message to serialize and publish (must respond to :to_s(log_filter,
|
338
|
+
# protocol_version) unless :no_serialize specified; if responds to :type, :from, :token,
|
339
|
+
# and/or :one_way, these value are used if this message is returned as non-deliverable)
|
340
|
+
# message(String):: Serialized message to be published
|
341
|
+
# options(Hash):: Publish options -- standard AMQP ones plus
|
342
|
+
# :no_serialize(Boolean):: Do not serialize packet because it is already serialized
|
343
|
+
# :log_filter(Array(Symbol)):: Filters to be applied in to_s when logging packet to :info
|
344
|
+
# :log_data(String):: Additional data to display at end of log entry
|
345
|
+
# :no_log(Boolean):: Disable publish logging unless debug level
|
346
|
+
#
|
347
|
+
# === Return
|
348
|
+
# (Boolean):: true if publish successfully, otherwise false
|
349
|
+
def publish(exchange, packet, message, options = {})
|
350
|
+
return false unless connected?
|
351
|
+
begin
|
352
|
+
exchange_options = exchange[:options] || {}
|
353
|
+
unless options[:no_serialize]
|
354
|
+
log_data = ""
|
355
|
+
unless options[:no_log] && logger.level != :debug
|
356
|
+
re = "RE-" if packet.respond_to?(:tries) && !packet.tries.empty?
|
357
|
+
log_filter = options[:log_filter] unless logger.level == :debug
|
358
|
+
log_data = "#{re}SEND #{@alias} #{packet.to_s(log_filter, :send_version)}"
|
359
|
+
if logger.level == :debug
|
360
|
+
log_data += ", publish options #{options.inspect}, exchange #{exchange[:name]}, " +
|
361
|
+
"type #{exchange[:type]}, options #{exchange[:options].inspect}"
|
362
|
+
end
|
363
|
+
log_data += ", #{options[:log_data]}" if options[:log_data]
|
364
|
+
logger.info(log_data) unless log_data.empty?
|
365
|
+
end
|
366
|
+
end
|
367
|
+
delete_amqp_resources(exchange[:type], exchange[:name]) if exchange_options[:declare]
|
368
|
+
@channel.__send__(exchange[:type], exchange[:name], exchange_options).publish(message, options)
|
369
|
+
true
|
370
|
+
rescue Exception => e
|
371
|
+
logger.exception("Failed publishing to exchange #{exchange.inspect} on broker #{@alias}", e, :trace)
|
372
|
+
@exceptions.track("publish", e)
|
373
|
+
false
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
# Provide callback to be activated when broker returns a message that could not be delivered
|
378
|
+
# A message published with :mandatory => true is returned if the exchange does not have any associated queues
|
379
|
+
# or if all the associated queues do not have any consumers
|
380
|
+
# A message published with :immediate => true is returned for the same reasons as :mandatory plus if all
|
381
|
+
# of the queues associated with the exchange are not immediately ready to consume the message
|
382
|
+
#
|
383
|
+
# === Block
|
384
|
+
# Optional block with following parameters to be called when a message is returned
|
385
|
+
# to(String):: Queue to which message was published
|
386
|
+
# reason(String):: Reason for return
|
387
|
+
# "NO_ROUTE" - queue does not exist
|
388
|
+
# "NO_CONSUMERS" - queue exists but it has no consumers, or if :immediate was specified,
|
389
|
+
# all consumers are not immediately ready to consume
|
390
|
+
# "ACCESS_REFUSED" - queue not usable because broker is in the process of stopping service
|
391
|
+
# message(String):: Returned serialized message
|
392
|
+
#
|
393
|
+
# === Return
|
394
|
+
# true:: Always return true
|
395
|
+
def return_message
|
396
|
+
@channel.return_message do |info, message|
|
397
|
+
begin
|
398
|
+
to = if info.exchange && !info.exchange.empty? then info.exchange else info.routing_key end
|
399
|
+
reason = info.reply_text
|
400
|
+
logger.debug("RETURN #{@alias} because #{reason} for #{to}")
|
401
|
+
yield(to, reason, message) if block_given?
|
402
|
+
rescue Exception => e
|
403
|
+
logger.exception("Failed return #{info.inspect} of message from broker #{@alias}", e, :trace)
|
404
|
+
@exceptions.track("return", e)
|
405
|
+
end
|
406
|
+
end
|
407
|
+
true
|
408
|
+
end
|
409
|
+
|
410
|
+
# Delete queue
|
411
|
+
#
|
412
|
+
# === Parameters
|
413
|
+
# name(String):: Queue name
|
414
|
+
# options(Hash):: Queue declare options
|
415
|
+
#
|
416
|
+
# === Return
|
417
|
+
# (Boolean):: true if queue was successfully deleted, otherwise false
|
418
|
+
def delete(name, options = {})
|
419
|
+
deleted = false
|
420
|
+
if usable?
|
421
|
+
begin
|
422
|
+
@queues.reject! do |q|
|
423
|
+
if q.name == name
|
424
|
+
@channel.queue(name, options.merge(:no_declare => true)).delete
|
425
|
+
deleted = true
|
426
|
+
end
|
427
|
+
end
|
428
|
+
unless deleted
|
429
|
+
# Allowing declare to happen since queue may not exist and do not want NOT_FOUND
|
430
|
+
# failure to cause AMQP channel to close
|
431
|
+
@channel.queue(name, options).delete
|
432
|
+
deleted = true
|
433
|
+
end
|
434
|
+
rescue Exception => e
|
435
|
+
logger.exception("Failed deleting queue #{name.inspect} on broker #{@alias}", e, :trace)
|
436
|
+
@exceptions.track("delete", e)
|
437
|
+
end
|
438
|
+
end
|
439
|
+
deleted
|
440
|
+
end
|
441
|
+
|
442
|
+
# Delete resources from local AMQP cache
|
443
|
+
#
|
444
|
+
# === Parameters
|
445
|
+
# type(Symbol):: Type of AMQP object
|
446
|
+
# name(String):: Name of object
|
447
|
+
#
|
448
|
+
# === Return
|
449
|
+
# true:: Always return true
|
450
|
+
def delete_amqp_resources(type, name)
|
451
|
+
@channel.__send__(type == :queue ? :queues : :exchanges).delete(name)
|
452
|
+
true
|
453
|
+
end
|
454
|
+
|
455
|
+
# Close broker connection
|
456
|
+
#
|
457
|
+
# === Parameters
|
458
|
+
# propagate(Boolean):: Whether to propagate connection status updates, defaults to true
|
459
|
+
# normal(Boolean):: Whether this is a normal close vs. a failed connection, defaults to true
|
460
|
+
# log(Boolean):: Whether to log that closing, defaults to true
|
461
|
+
#
|
462
|
+
# === Block
|
463
|
+
# Optional block with no parameters to be called after connection closed
|
464
|
+
#
|
465
|
+
# === Return
|
466
|
+
# true:: Always return true
|
467
|
+
def close(propagate = true, normal = true, log = true, &blk)
|
468
|
+
final_status = normal ? :closed : :failed
|
469
|
+
if ![:closed, :failed].include?(@status)
|
470
|
+
begin
|
471
|
+
logger.info("[stop] Closed connection to broker #{@alias}") if log
|
472
|
+
update_status(final_status) if propagate
|
473
|
+
@connection.close do
|
474
|
+
@status = final_status
|
475
|
+
yield if block_given?
|
476
|
+
end
|
477
|
+
rescue Exception => e
|
478
|
+
logger.exception("Failed to close broker #{@alias}", e, :trace)
|
479
|
+
@exceptions.track("close", e)
|
480
|
+
@status = final_status
|
481
|
+
yield if block_given?
|
482
|
+
end
|
483
|
+
else
|
484
|
+
@status = final_status
|
485
|
+
yield if block_given?
|
486
|
+
end
|
487
|
+
true
|
488
|
+
end
|
489
|
+
|
490
|
+
# Get broker client information summarizing its status
|
491
|
+
#
|
492
|
+
# === Return
|
493
|
+
# (Hash):: Status of broker with keys
|
494
|
+
# :identity(String):: Serialized identity
|
495
|
+
# :alias(String):: Alias used in logs
|
496
|
+
# :status(Symbol):: Status of connection
|
497
|
+
# :disconnects(Integer):: Number of times lost connection
|
498
|
+
# :failures(Integer):: Number of times connect failed
|
499
|
+
# :retries(Integer):: Number of attempts to connect after failure
|
500
|
+
def summary
|
501
|
+
{
|
502
|
+
:identity => @identity,
|
503
|
+
:alias => @alias,
|
504
|
+
:status => @status,
|
505
|
+
:retries => @retries,
|
506
|
+
:disconnects => @disconnects.total,
|
507
|
+
:failures => @failures.total,
|
508
|
+
}
|
509
|
+
end
|
510
|
+
|
511
|
+
# Get broker client statistics
|
512
|
+
#
|
513
|
+
# === Return
|
514
|
+
# (Hash):: Broker client stats with keys
|
515
|
+
# "alias"(String):: Broker alias
|
516
|
+
# "identity"(String):: Broker identity
|
517
|
+
# "status"(Status):: Status of connection
|
518
|
+
# "disconnect last"(Hash|nil):: Last disconnect information with key "elapsed", or nil if none
|
519
|
+
# "disconnects"(Integer|nil):: Number of times lost connection, or nil if none
|
520
|
+
# "failure last"(Hash|nil):: Last connect failure information with key "elapsed", or nil if none
|
521
|
+
# "failures"(Integer|nil):: Number of failed attempts to connect to broker, or nil if none
|
522
|
+
def stats
|
523
|
+
{
|
524
|
+
"alias" => @alias,
|
525
|
+
"identity" => @identity,
|
526
|
+
"status" => @status.to_s,
|
527
|
+
"disconnect last" => @disconnects.last,
|
528
|
+
"disconnects" => RightSupport::Stats.nil_if_zero(@disconnects.total),
|
529
|
+
"failure last" => @failures.last,
|
530
|
+
"failures" => RightSupport::Stats.nil_if_zero(@failures.total),
|
531
|
+
"retries" => RightSupport::Stats.nil_if_zero(@retries)
|
532
|
+
}
|
533
|
+
end
|
534
|
+
|
535
|
+
# Callback from AMQP with connection status or from HABrokerClient
|
536
|
+
# Makes client callback with :connected or :disconnected status if boundary crossed
|
537
|
+
#
|
538
|
+
# === Parameters
|
539
|
+
# status(Symbol):: Status of connection (:connected, :disconnected, :stopping, :failed, :closed)
|
540
|
+
#
|
541
|
+
# === Return
|
542
|
+
# true:: Always return true
|
543
|
+
def update_status(status)
|
544
|
+
# Do not let closed connection regress to failed
|
545
|
+
return true if status == :failed && @status == :closed
|
546
|
+
|
547
|
+
# Wait until connection is ready (i.e. handshake with broker is completed) before
|
548
|
+
# changing our status to connected
|
549
|
+
return true if status == :connected
|
550
|
+
status = :connected if status == :ready
|
551
|
+
|
552
|
+
before = @status
|
553
|
+
@status = status
|
554
|
+
|
555
|
+
if status == :connected
|
556
|
+
update_success
|
557
|
+
elsif status == :failed
|
558
|
+
update_failure
|
559
|
+
elsif status == :disconnected && before != :disconnected
|
560
|
+
@disconnects.update
|
561
|
+
end
|
562
|
+
|
563
|
+
unless status == before || @options[:update_status_callback].nil?
|
564
|
+
@options[:update_status_callback].call(self, before == :connected)
|
565
|
+
end
|
566
|
+
true
|
567
|
+
end
|
568
|
+
|
569
|
+
protected
|
570
|
+
|
571
|
+
# Connect to broker and register for status updates
|
572
|
+
# Also set prefetch value if specified
|
573
|
+
#
|
574
|
+
# === Parameters
|
575
|
+
# address(Hash):: Broker address
|
576
|
+
# :host(String:: IP host name or address
|
577
|
+
# :port(Integer):: TCP port number for individual broker
|
578
|
+
# :index(String):: Unique index for broker within given set for use in forming alias
|
579
|
+
# reconnect_interval(Integer):: Number of seconds between reconnect attempts
|
580
|
+
#
|
581
|
+
# === Return
|
582
|
+
# true:: Always return true
|
583
|
+
def connect(address, reconnect_interval)
|
584
|
+
begin
|
585
|
+
logger.info("[setup] Connecting to broker #{@identity}, alias #{@alias}")
|
586
|
+
@status = :connecting
|
587
|
+
@connection = AMQP.connect(:user => @options[:user],
|
588
|
+
:pass => @options[:pass],
|
589
|
+
:vhost => @options[:vhost],
|
590
|
+
:host => address[:host],
|
591
|
+
:port => address[:port],
|
592
|
+
:identity => @identity,
|
593
|
+
:insist => @options[:insist] || false,
|
594
|
+
:heartbeat => @options[:heartbeat],
|
595
|
+
:reconnect_delay => lambda { rand(reconnect_interval) },
|
596
|
+
:reconnect_interval => reconnect_interval)
|
597
|
+
@channel = MQ.new(@connection)
|
598
|
+
@channel.__send__(:connection).connection_status { |status| update_status(status) }
|
599
|
+
@channel.prefetch(@options[:prefetch]) if @options[:prefetch]
|
600
|
+
rescue Exception => e
|
601
|
+
@status = :failed
|
602
|
+
@failures.update
|
603
|
+
logger.exception("Failed connecting to broker #{@alias}", e, :trace)
|
604
|
+
@exceptions.track("connect", e)
|
605
|
+
@connection.close if @connection
|
606
|
+
end
|
607
|
+
end
|
608
|
+
|
609
|
+
# Receive message by unserializing it, checking that it is an acceptable type, and logging accordingly
|
610
|
+
#
|
611
|
+
# === Parameters
|
612
|
+
# queue(String):: Name of queue
|
613
|
+
# message(String):: Serialized packet
|
614
|
+
# options(Hash):: Subscribe options:
|
615
|
+
# (packet class)(Array(Symbol)):: Filters to be applied in to_s when logging packet to :info,
|
616
|
+
# only packet classes specified are accepted, others are not processed but are logged with error
|
617
|
+
# :category(String):: Packet category description to be used in error messages
|
618
|
+
# :log_data(String):: Additional data to display at end of log entry
|
619
|
+
# :no_log(Boolean):: Disable receive logging unless debug level
|
620
|
+
#
|
621
|
+
# === Return
|
622
|
+
# (Packet|nil):: Unserialized packet or nil if not of right type or if there is an exception
|
623
|
+
def receive(queue, message, options = {})
|
624
|
+
begin
|
625
|
+
received_at = Time.now.to_f
|
626
|
+
packet = @serializer.load(message)
|
627
|
+
if options.key?(packet.class)
|
628
|
+
unless options[:no_log] && logger.level != :debug
|
629
|
+
re = "RE-" if packet.respond_to?(:tries) && !packet.tries.empty?
|
630
|
+
packet.received_at = received_at if packet.respond_to?(:received_at)
|
631
|
+
log_filter = options[packet.class] unless logger.level == :debug
|
632
|
+
logger.info("#{re}RECV #{@alias} #{packet.to_s(log_filter, :recv_version)} #{options[:log_data]}")
|
633
|
+
end
|
634
|
+
packet
|
635
|
+
else
|
636
|
+
category = options[:category] + " " if options[:category]
|
637
|
+
logger.warning("Received invalid #{category}packet type from queue #{queue} on broker #{@alias}: #{packet.class}\n" + caller.join("\n"))
|
638
|
+
nil
|
639
|
+
end
|
640
|
+
rescue Exception => e
|
641
|
+
# TODO Taking advantage of Serializer knowledge here even though out of scope
|
642
|
+
trace = e.class.name =~ /SerializationError/ ? :caller : :trace
|
643
|
+
logger.exception("Failed receiving from queue #{queue} on #{@alias}", e, trace)
|
644
|
+
@exceptions.track("receive", e)
|
645
|
+
@options[:exception_on_receive_callback].call(message, e) if @options[:exception_on_receive_callback]
|
646
|
+
nil
|
647
|
+
end
|
648
|
+
end
|
649
|
+
|
650
|
+
# Make status updates for connect success
|
651
|
+
#
|
652
|
+
# === Return
|
653
|
+
# true:: Always return true
|
654
|
+
def update_success
|
655
|
+
@last_failed = false
|
656
|
+
@retries = 0
|
657
|
+
true
|
658
|
+
end
|
659
|
+
|
660
|
+
# Make status updates for connect failure
|
661
|
+
#
|
662
|
+
# === Return
|
663
|
+
# true:: Always return true
|
664
|
+
def update_failure
|
665
|
+
logger.exception("Failed to connect to broker #{@alias}")
|
666
|
+
if @last_failed
|
667
|
+
@retries += 1
|
668
|
+
else
|
669
|
+
@last_failed = true
|
670
|
+
@retries = 0
|
671
|
+
@failures.update
|
672
|
+
end
|
673
|
+
true
|
674
|
+
end
|
675
|
+
|
676
|
+
# Execute packet receive callback, make it a separate method to ease instrumentation
|
677
|
+
#
|
678
|
+
# === Parameters
|
679
|
+
# callback(Proc):: Proc to run
|
680
|
+
# args(Array):: Array of pass-through arguments
|
681
|
+
#
|
682
|
+
# === Return
|
683
|
+
# (Object):: Callback return value
|
684
|
+
def execute_callback(callback, *args)
|
685
|
+
(callback.arity == 2 ? callback.call(*args[0, 2]) : callback.call(*args)) if callback
|
686
|
+
end
|
687
|
+
|
688
|
+
end # BrokerClient
|
689
|
+
|
690
|
+
end # RightAMQP
|