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.
@@ -0,0 +1,3 @@
1
+ module AMQP
2
+ VERSION = '0.6.7'
3
+ end
@@ -0,0 +1,7 @@
1
+ unless defined?(BlankSlate)
2
+ class BlankSlate < BasicObject; end if defined?(BasicObject)
3
+
4
+ class BlankSlate #:nodoc:
5
+ instance_methods.each { |m| undef_method m unless m =~ /^__/ }
6
+ end
7
+ end
@@ -0,0 +1,8 @@
1
+ begin
2
+ require 'eventmachine'
3
+ rescue LoadError
4
+ require 'rubygems'
5
+ require 'eventmachine'
6
+ end
7
+
8
+ require File.expand_path('../emfork', __FILE__)
@@ -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