right_amqp 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,1185 @@
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 multiple AMQP brokers used together to achieve a high availability
26
+ # messaging routing service
27
+ class HABrokerClient
28
+
29
+ include RightSupport::Log::Mixin
30
+
31
+ class NoUserData < Exception; end
32
+ class NoBrokerHosts < Exception; end
33
+ class NoConnectedBrokers < Exception; end
34
+
35
+ # Message publishing context
36
+ class Context
37
+
38
+ # (String) Message class name in lower snake case
39
+ attr_reader :name
40
+
41
+ # (String) Request type if applicable
42
+ attr_reader :type
43
+
44
+ # (String) Original sender of message if applicable
45
+ attr_reader :from
46
+
47
+ # (String) Generated message identifier if applicable
48
+ attr_reader :token
49
+
50
+ # (Boolean) Whether the packet is one that does not have an associated response
51
+ attr_reader :one_way
52
+
53
+ # (Hash) Options used to publish message
54
+ attr_reader :options
55
+
56
+ # (Array) Identity of candidate brokers when message was published
57
+ attr_reader :brokers
58
+
59
+ # (Array) Identity of brokers that have failed to deliver message with last one at end
60
+ attr_reader :failed
61
+
62
+ # Create context
63
+ #
64
+ # === Parameters
65
+ # packet(Packet):: Packet being published
66
+ # options(Hash):: Publish options
67
+ # brokers(Array):: Identity of candidate brokers
68
+ def initialize(packet, options, brokers)
69
+ @name = (packet.respond_to?(:name) ? packet.name : packet.class.name.snake_case)
70
+ @type = (packet.type if packet.respond_to?(:type) && packet.type != packet.class)
71
+ @from = (packet.from if packet.respond_to?(:from))
72
+ @token = (packet.token if packet.respond_to?(:token))
73
+ @one_way = (packet.respond_to?(:one_way) ? packet.one_way : true)
74
+ @options = options
75
+ @brokers = brokers
76
+ @failed = []
77
+ end
78
+
79
+ # Record delivery failure
80
+ #
81
+ # === Parameters
82
+ # identity(String):: Identity of broker that failed delivery
83
+ #
84
+ # === Return
85
+ # true:: Always return true
86
+ def record_failure(identity)
87
+ @failed << identity
88
+ end
89
+
90
+ end
91
+
92
+ # Default number of seconds between reconnect attempts
93
+ RECONNECT_INTERVAL = 60
94
+
95
+ # (Array(Broker)) Priority ordered list of AMQP broker clients (exposed only for unit test purposes)
96
+ attr_accessor :brokers
97
+
98
+ # Create connections to all configured AMQP brokers
99
+ # The constructed broker client list is in priority order
100
+ #
101
+ # === Parameters
102
+ # serializer(Serializer):: Serializer used for marshaling packets being published or
103
+ # unmarshaling received messages to packets (responds to :dump and :load); if nil, has
104
+ # same effect as setting subscribe option :no_serialize and publish option :no_unserialize
105
+ # options(Hash):: Configuration options
106
+ # :user(String):: User name
107
+ # :pass(String):: Password
108
+ # :vhost(String):: Virtual host path name
109
+ # :insist(Boolean):: Whether to suppress redirection of connection
110
+ # :reconnect_interval(Integer):: Number of seconds between reconnect attempts, defaults to RECONNECT_INTERVAL
111
+ # :heartbeat(Integer):: Number of seconds between AMQP connection heartbeats used to keep
112
+ # connection alive (e.g., when AMQP broker is behind a firewall), nil or 0 means disable
113
+ # :host{String):: Comma-separated list of AMQP broker host names; if only one, it is reapplied
114
+ # to successive ports; if none, defaults to localhost; each host may be followed by ':'
115
+ # and a short string to be used as a broker index; the index defaults to the list index,
116
+ # e.g., "host_a:0, host_c:2"
117
+ # :port(String|Integer):: Comma-separated list of AMQP broker port numbers corresponding to :host list;
118
+ # if only one, it is incremented and applied to successive hosts; if none, defaults to AMQP::PORT
119
+ # :prefetch(Integer):: Maximum number of messages the AMQP broker is to prefetch for the agent
120
+ # before it receives an ack. Value 1 ensures that only last unacknowledged gets redelivered
121
+ # if the agent crashes. Value 0 means unlimited prefetch.
122
+ # :order(Symbol):: Broker selection order when publishing a message: :random or :priority,
123
+ # defaults to :priority, value can be overridden on publish call
124
+ # :exception_callback(Proc):: Callback activated on exception events with parameters
125
+ # exception(Exception):: Exception
126
+ # message(Packet):: Message being processed
127
+ # client(HABrokerClient):: Reference to this client
128
+ # :exception_on_receive_callback(Proc):: Callback activated on a receive exception with parameters
129
+ # message(String):: Message content that caused an exception
130
+ # exception(Exception):: Exception that was raised
131
+ #
132
+ # === Raise
133
+ # ArgumentError:: If :host and :port are not matched lists or if serializer does not respond
134
+ # to :dump and :load
135
+ def initialize(serializer, options = {})
136
+ @options = options.dup
137
+ @options[:update_status_callback] = lambda { |b, c| update_status(b, c) }
138
+ @options[:reconnect_interval] ||= RECONNECT_INTERVAL
139
+ @connection_status = {}
140
+ unless serializer.nil? || [:dump, :load].all? { |m| serializer.respond_to?(m) }
141
+ raise ArgumentError, "serializer must be a class/object that responds to :dump and :load"
142
+ end
143
+ @serializer = serializer
144
+ @published = Published.new
145
+ reset_stats
146
+ @select = @options[:order] || :priority
147
+ @brokers = connect_all
148
+ @closed = false
149
+ @brokers_hash = {}
150
+ @brokers.each { |b| @brokers_hash[b.identity] = b }
151
+ return_message { |i, r, m, t, c| handle_return(i, r, m, t, c) }
152
+ end
153
+
154
+ # Parse agent user data to extract broker host and port configuration
155
+ # An agent is permitted to only support using one broker
156
+ #
157
+ # === Parameters
158
+ # user_data(String):: Agent user data in <name>=<value>&<name>=<value>&... form
159
+ # with required name RS_rn_url and optional names RS_rn_host and RS_rn_port
160
+ #
161
+ # === Return
162
+ # (Array):: Broker hosts and ports as comma-separated list in priority order in the form
163
+ # <hostname>:<index>,<hostname>:<index>,...
164
+ # <port>:<index>,<port>:<index>,... or nil if none specified
165
+ #
166
+ # === Raise
167
+ # NoUserData:: If the user data is missing
168
+ # NoBrokerHosts:: If no brokers could be extracted from the user data
169
+ def self.parse_user_data(user_data)
170
+ raise NoUserData.new("User data is missing") if user_data.nil? || user_data.empty?
171
+ hosts = ""
172
+ ports = nil
173
+ user_data.split("&").each do |data|
174
+ name, value = data.split("=")
175
+ if name == "RS_rn_url"
176
+ h = value.split("@").last.split("/").first
177
+ # Translate host name used by very old agents using only one broker
178
+ h = "broker1-1.rightscale.com" if h == "broker.rightscale.com"
179
+ hosts = h + hosts
180
+ end
181
+ if name == "RS_rn_host"
182
+ hosts << value
183
+ end
184
+ if name == "RS_rn_port"
185
+ ports = value
186
+ end
187
+ end
188
+ raise NoBrokerHosts.new("No brokers found in user data") if hosts.empty?
189
+ [hosts, ports]
190
+ end
191
+
192
+ # Parse host and port information to form list of broker address information
193
+ #
194
+ # === Parameters
195
+ # host{String):: Comma-separated list of broker host names; if only one, it is reapplied
196
+ # to successive ports; if none, defaults to localhost; each host may be followed by ':'
197
+ # and a short string to be used as a broker index; the index defaults to the list index,
198
+ # e.g., "host_a:0, host_c:2"
199
+ # port(String|Integer):: Comma-separated list of broker port numbers corresponding to :host list;
200
+ # if only one, it is incremented and applied to successive hosts; if none, defaults to AMQP::PORT
201
+ #
202
+ # === Returns
203
+ # (Array(Hash)):: List of broker addresses with keys :host, :port, :index
204
+ #
205
+ # === Raise
206
+ # ArgumentError:: If host and port are not matched lists
207
+ def self.addresses(host, port)
208
+ hosts = if host && !host.empty? then host.split(/,\s*/) else [ "localhost" ] end
209
+ ports = if port && port.size > 0 then port.to_s.split(/,\s*/) else [ ::AMQP::PORT ] end
210
+ if hosts.size != ports.size && hosts.size != 1 && ports.size != 1
211
+ raise ArgumentError.new("Unmatched AMQP host/port lists -- hosts: #{host.inspect} ports: #{port.inspect}")
212
+ end
213
+ i = -1
214
+ if hosts.size > 1
215
+ hosts.map do |host|
216
+ i += 1
217
+ h = host.split(/:\s*/)
218
+ port = if ports[i] then ports[i].to_i else ports[0].to_i end
219
+ port = port.to_s.split(/:\s*/)[0]
220
+ {:host => h[0], :port => port.to_i, :index => (h[1] || i.to_s).to_i}
221
+ end
222
+ else
223
+ ports.map do |port|
224
+ i += 1
225
+ p = port.to_s.split(/:\s*/)
226
+ host = if hosts[i] then hosts[i] else hosts[0] end
227
+ host = host.split(/:\s*/)[0]
228
+ {:host => host, :port => p[0].to_i, :index => (p[1] || i.to_s).to_i}
229
+ end
230
+ end
231
+ end
232
+
233
+ # Parse host and port information to form list of broker identities
234
+ #
235
+ # === Parameters
236
+ # host{String):: Comma-separated list of broker host names; if only one, it is reapplied
237
+ # to successive ports; if none, defaults to localhost; each host may be followed by ':'
238
+ # and a short string to be used as a broker index; the index defaults to the list index,
239
+ # e.g., "host_a:0, host_c:2"
240
+ # port(String|Integer):: Comma-separated list of broker port numbers corresponding to :host list;
241
+ # if only one, it is incremented and applied to successive hosts; if none, defaults to AMQP::PORT
242
+ #
243
+ # === Returns
244
+ # (Array):: Identity of each broker
245
+ #
246
+ # === Raise
247
+ # ArgumentError:: If host and port are not matched lists
248
+ def self.identities(host, port = nil)
249
+ addresses(host, port).map { |a| identity(a[:host], a[:port]) }
250
+ end
251
+
252
+ # Construct a broker serialized identity from its host and port of the form
253
+ # rs-broker-host-port, with any '-'s in host replaced by '~'
254
+ #
255
+ # === Parameters
256
+ # host{String):: IP host name or address for individual broker
257
+ # port(Integer):: TCP port number for individual broker, defaults to ::AMQP::PORT
258
+ #
259
+ # === Returns
260
+ # (String):: Broker serialized identity
261
+ def self.identity(host, port = ::AMQP::PORT)
262
+ "rs-broker-#{host.gsub('-', '~')}-#{port.to_i}"
263
+ end
264
+
265
+ # Break broker serialized identity down into individual parts if exists
266
+ #
267
+ # === Parameters
268
+ # id(Integer|String):: Broker alias or serialized identity
269
+ #
270
+ # === Return
271
+ # (Array):: Host, port, index, and priority, or all nil if broker not found
272
+ def identity_parts(id)
273
+ @brokers.each do |b|
274
+ return [b.host, b.port, b.index, priority(b.identity)] if b.identity == id || b.alias == id
275
+ end
276
+ [nil, nil, nil, nil]
277
+ end
278
+
279
+ # Convert broker identities to aliases
280
+ #
281
+ # === Parameters
282
+ # identities(Array):: Broker identities
283
+ #
284
+ # === Return
285
+ # (Array):: Broker aliases
286
+ def aliases(identities)
287
+ identities.map { |i| alias_(i) }
288
+ end
289
+
290
+ # Convert broker serialized identity to its alias
291
+ #
292
+ # === Parameters
293
+ # identity(String):: Broker serialized identity
294
+ #
295
+ # === Return
296
+ # (String|nil):: Broker alias, or nil if not a known broker
297
+ def alias_(identity)
298
+ @brokers_hash[identity].alias rescue nil
299
+ end
300
+
301
+ # Form string of hosts and associated indices
302
+ #
303
+ # === Return
304
+ # (String):: Comma separated list of host:index
305
+ def hosts
306
+ @brokers.map { |b| "#{b.host}:#{b.index}" }.join(",")
307
+ end
308
+
309
+ # Form string of ports and associated indices
310
+ #
311
+ # === Return
312
+ # (String):: Comma separated list of port:index
313
+ def ports
314
+ @brokers.map { |b| "#{b.port}:#{b.index}" }.join(",")
315
+ end
316
+
317
+ # Get broker serialized identity if client exists
318
+ #
319
+ # === Parameters
320
+ # id(Integer|String):: Broker alias or serialized identity
321
+ #
322
+ # === Return
323
+ # (String|nil):: Broker serialized identity if client found, otherwise nil
324
+ def get(id)
325
+ @brokers.each { |b| return b.identity if b.identity == id || b.alias == id }
326
+ nil
327
+ end
328
+
329
+ # Check whether connected to broker
330
+ #
331
+ # === Parameters
332
+ # identity{String):: Broker serialized identity
333
+ #
334
+ # === Return
335
+ # (Boolean):: true if connected to broker, otherwise false, or nil if broker unknown
336
+ def connected?(identity)
337
+ @brokers_hash[identity].connected? rescue nil
338
+ end
339
+
340
+ # Get serialized identity of connected brokers
341
+ #
342
+ # === Return
343
+ # (Array):: Serialized identity of connected brokers
344
+ def connected
345
+ @brokers.inject([]) { |c, b| if b.connected? then c << b.identity else c end }
346
+ end
347
+
348
+ # Get serialized identity of brokers that are usable, i.e., connecting or confirmed connected
349
+ #
350
+ # === Return
351
+ # (Array):: Serialized identity of usable brokers
352
+ def usable
353
+ each_usable.map { |b| b.identity }
354
+ end
355
+
356
+ # Get serialized identity of unusable brokers
357
+ #
358
+ # === Return
359
+ # (Array):: Serialized identity of unusable brokers
360
+ def unusable
361
+ @brokers.map { |b| b.identity } - each_usable.map { |b| b.identity }
362
+ end
363
+
364
+ # Get serialized identity of all brokers
365
+ #
366
+ # === Return
367
+ # (Array):: Serialized identity of all brokers
368
+ def all
369
+ @brokers.map { |b| b.identity }
370
+ end
371
+
372
+ # Get serialized identity of failed broker clients, i.e., ones that were never successfully
373
+ # connected, not ones that are just disconnected
374
+ #
375
+ # === Return
376
+ # (Array):: Serialized identity of failed broker clients
377
+ def failed
378
+ @brokers.inject([]) { |c, b| b.failed? ? c << b.identity : c }
379
+ end
380
+
381
+ # Change connection heartbeat frequency to be used for any new connections
382
+ #
383
+ # === Parameters
384
+ # heartbeat(Integer):: Number of seconds between AMQP connection heartbeats used to keep
385
+ # connection alive (e.g., when AMQP broker is behind a firewall), nil or 0 means disable
386
+ #
387
+ # === Return
388
+ # (Integer|nil):: New heartbeat setting
389
+ def heartbeat=(heartbeat)
390
+ @options[:heartbeat] = heartbeat
391
+ end
392
+
393
+ # Make new connection to broker at specified address unless already connected
394
+ # or currently connecting
395
+ #
396
+ # === Parameters
397
+ # host{String):: IP host name or address for individual broker
398
+ # port(Integer):: TCP port number for individual broker
399
+ # index(Integer):: Unique index for broker within set for use in forming alias
400
+ # priority(Integer|nil):: Priority position of this broker in set for use by this agent
401
+ # with nil or a value that would leave a gap in the list meaning add to end of list
402
+ # force(Boolean):: Reconnect even if already connected
403
+ #
404
+ # === Block
405
+ # Optional block with following parameters to be called after initiating the connection
406
+ # unless already connected to this broker:
407
+ # identity(String):: Broker serialized identity
408
+ #
409
+ # === Return
410
+ # (Boolean):: true if connected, false if no connect attempt made
411
+ #
412
+ # === Raise
413
+ # Exception:: If host and port do not match an existing broker but index does
414
+ def connect(host, port, index, priority = nil, force = false, &blk)
415
+ identity = self.class.identity(host, port)
416
+ existing = @brokers_hash[identity]
417
+ if existing && existing.usable? && !force
418
+ logger.info("Ignored request to reconnect #{identity} because already #{existing.status.to_s}")
419
+ false
420
+ else
421
+ old_identity = identity
422
+ @brokers.each do |b|
423
+ if index == b.index
424
+ # Changing host and/or port of existing broker client
425
+ old_identity = b.identity
426
+ break
427
+ end
428
+ end unless existing
429
+
430
+ address = {:host => host, :port => port, :index => index}
431
+ broker = BrokerClient.new(identity, address, @serializer, @exceptions, @options, existing)
432
+ p = priority(old_identity)
433
+ if priority && priority < p
434
+ @brokers.insert(priority, broker)
435
+ elsif priority && priority > p
436
+ logger.info("Reduced priority setting for broker #{identity} from #{priority} to #{p} to avoid gap in list")
437
+ @brokers.insert(p, broker)
438
+ else
439
+ @brokers[p].close if @brokers[p]
440
+ @brokers[p] = broker
441
+ end
442
+ @brokers_hash[identity] = broker
443
+ yield broker.identity if block_given?
444
+ true
445
+ end
446
+ end
447
+
448
+ # Subscribe an AMQP queue to an AMQP exchange on all broker clients that are connected or still connecting
449
+ # Allow connecting here because subscribing may happen before all have confirmed connected
450
+ # Do not wait for confirmation from broker client that subscription is complete
451
+ # When a message is received, acknowledge, unserialize, and log it as specified
452
+ # If the message is unserialized and it is not of the right type, it is dropped after logging a warning
453
+ #
454
+ # === Parameters
455
+ # queue(Hash):: AMQP queue being subscribed with keys :name and :options,
456
+ # which are the standard AMQP ones plus
457
+ # :no_declare(Boolean):: Whether to skip declaring this queue on the broker
458
+ # to cause its creation; for use when client does not have permission to create or
459
+ # knows the queue already exists and wants to avoid declare overhead
460
+ # exchange(Hash|nil):: AMQP exchange to subscribe to with keys :type, :name, and :options,
461
+ # nil means use empty exchange by directly subscribing to queue; the :options are the
462
+ # standard AMQP ones plus
463
+ # :no_declare(Boolean):: Whether to skip declaring this exchange on the broker
464
+ # to cause its creation; for use when client does not have create permission or
465
+ # knows the exchange already exists and wants to avoid declare overhead
466
+ # options(Hash):: Subscribe options:
467
+ # :ack(Boolean):: Explicitly acknowledge received messages to AMQP
468
+ # :no_unserialize(Boolean):: Do not unserialize message, this is an escape for special
469
+ # situations like enrollment, also implicitly disables receive filtering and logging;
470
+ # this option is implicitly invoked if initialize without a serializer
471
+ # (packet class)(Array(Symbol)):: Filters to be applied in to_s when logging packet to :info,
472
+ # only packet classes specified are accepted, others are not processed but are logged with error
473
+ # :category(String):: Packet category description to be used in error messages
474
+ # :log_data(String):: Additional data to display at end of log entry
475
+ # :no_log(Boolean):: Disable receive logging unless debug level
476
+ # :exchange2(Hash):: Additional exchange to which same queue is to be bound
477
+ # :brokers(Array):: Identity of brokers for which to subscribe, defaults to all usable if nil or empty
478
+ #
479
+ # === Block
480
+ # Block with following parameters to be called each time exchange matches a message to the queue:
481
+ # identity(String):: Serialized identity of broker delivering the message
482
+ # message(Packet|String):: Message received, which is unserialized unless :no_unserialize was specified
483
+ # header(AMQP::Protocol::Header):: Message header (optional block parameter)
484
+ #
485
+ # === Return
486
+ # identities(Array):: Identity of brokers where successfully subscribed
487
+ def subscribe(queue, exchange = nil, options = {}, &blk)
488
+ identities = []
489
+ brokers = options.delete(:brokers)
490
+ each_usable(brokers) { |b| identities << b.identity if b.subscribe(queue, exchange, options, &blk) }
491
+ logger.info("Could not subscribe to queue #{queue.inspect} on exchange #{exchange.inspect} " +
492
+ "on brokers #{each_usable(brokers).inspect} when selected #{brokers.inspect} " +
493
+ "from usable #{usable.inspect}") if identities.empty?
494
+ identities
495
+ end
496
+
497
+ # Unsubscribe from the specified queues on usable broker clients
498
+ # Silently ignore unknown queues
499
+ #
500
+ # === Parameters
501
+ # queue_names(Array):: Names of queues previously subscribed to
502
+ # timeout(Integer):: Number of seconds to wait for all confirmations, defaults to no timeout
503
+ #
504
+ # === Block
505
+ # Optional block with no parameters to be called after all queues are unsubscribed
506
+ #
507
+ # === Return
508
+ # true:: Always return true
509
+ def unsubscribe(queue_names, timeout = nil, &blk)
510
+ count = each_usable.inject(0) do |c, b|
511
+ c + b.queues.inject(0) { |c, q| c + (queue_names.include?(q.name) ? 1 : 0) }
512
+ end
513
+ if count == 0
514
+ blk.call if blk
515
+ else
516
+ handler = CountedDeferrable.new(count, timeout)
517
+ handler.callback { blk.call if blk }
518
+ each_usable { |b| b.unsubscribe(queue_names) { handler.completed_one } }
519
+ end
520
+ true
521
+ end
522
+
523
+ # Declare queue or exchange object but do not subscribe to it
524
+ #
525
+ # === Parameters
526
+ # type(Symbol):: Type of object: :queue, :direct, :fanout or :topic
527
+ # name(String):: Name of object
528
+ # options(Hash):: Standard AMQP declare options plus
529
+ # :brokers(Array):: Identity of brokers for which to declare, defaults to all usable if nil or empty
530
+ #
531
+ # === Return
532
+ # identities(Array):: Identity of brokers where successfully declared
533
+ def declare(type, name, options = {})
534
+ identities = []
535
+ brokers = options.delete(:brokers)
536
+ each_usable(brokers) { |b| identities << b.identity if b.declare(type, name, options) }
537
+ logger.info("Could not declare #{type.to_s} #{name.inspect} on brokers #{each_usable(brokers).inspect} " +
538
+ "when selected #{brokers.inspect} from usable #{usable.inspect}") if identities.empty?
539
+ identities
540
+ end
541
+
542
+ # Publish message to AMQP exchange of first connected broker
543
+ #
544
+ # === Parameters
545
+ # exchange(Hash):: AMQP exchange to subscribe to with keys :type, :name, and :options,
546
+ # which are the standard AMQP ones plus
547
+ # :no_declare(Boolean):: Whether to skip declaring this exchange or queue on the broker
548
+ # to cause its creation; for use when client does not have create permission or
549
+ # knows the object already exists and wants to avoid declare overhead
550
+ # :declare(Boolean):: Whether to delete this exchange or queue from the AMQP cache
551
+ # to force it to be declared on the broker and thus be created if it does not exist
552
+ # packet(Packet):: Message to serialize and publish
553
+ # options(Hash):: Publish options -- standard AMQP ones plus
554
+ # :fanout(Boolean):: true means publish to all connected brokers
555
+ # :brokers(Array):: Identity of brokers selected for use, defaults to all home brokers
556
+ # if nil or empty
557
+ # :order(Symbol):: Broker selection order: :random or :priority,
558
+ # defaults to @select if :brokers is nil, otherwise defaults to :priority
559
+ # :no_serialize(Boolean):: Do not serialize packet because it is already serialized,
560
+ # this is an escape for special situations like enrollment, also implicitly disables
561
+ # publish logging; this option is implicitly invoked if initialize without a serializer
562
+ # :log_filter(Array(Symbol)):: Filters to be applied in to_s when logging packet to :info
563
+ # :log_data(String):: Additional data to display at end of log entry
564
+ # :no_log(Boolean):: Disable publish logging unless debug level
565
+ #
566
+ # === Return
567
+ # identities(Array):: Identity of brokers where packet was successfully published
568
+ #
569
+ # === Raise
570
+ # NoConnectedBrokers:: If cannot find a connected broker
571
+ def publish(exchange, packet, options = {})
572
+ identities = []
573
+ no_serialize = options[:no_serialize] || @serializer.nil?
574
+ message = if no_serialize then packet else @serializer.dump(packet) end
575
+ brokers = use(options)
576
+ brokers.each do |b|
577
+ if b.publish(exchange, packet, message, options.merge(:no_serialize => no_serialize))
578
+ identities << b.identity
579
+ if options[:mandatory] && !no_serialize
580
+ context = Context.new(packet, options, brokers.map { |b| b.identity })
581
+ @published.store(message, context)
582
+ end
583
+ break unless options[:fanout]
584
+ end
585
+ end
586
+ if identities.empty?
587
+ selected = "selected " if options[:brokers]
588
+ list = aliases(brokers.map { |b| b.identity }).join(", ")
589
+ raise NoConnectedBrokers, "None of #{selected}brokers [#{list}] are usable for publishing"
590
+ end
591
+ identities
592
+ end
593
+
594
+ # Register callback to be activated when a broker returns a message that could not be delivered
595
+ # A message published with :mandatory => true is returned if the exchange does not have any associated queues
596
+ # or if all the associated queues do not have any consumers
597
+ # A message published with :immediate => true is returned for the same reasons as :mandatory plus if all
598
+ # of the queues associated with the exchange are not immediately ready to consume the message
599
+ # Remove any previously registered callback
600
+ #
601
+ # === Block
602
+ # Required block to be called when a message is returned with parameters
603
+ # identity(String):: Broker serialized identity
604
+ # reason(String):: Reason for return
605
+ # "NO_ROUTE" - queue does not exist
606
+ # "NO_CONSUMERS" - queue exists but it has no consumers, or if :immediate was specified,
607
+ # all consumers are not immediately ready to consume
608
+ # "ACCESS_REFUSED" - queue not usable because broker is in the process of stopping service
609
+ # message(String):: Returned serialized message
610
+ # to(String):: Queue to which message was published
611
+ # context(Context|nil):: Message publishing context, or nil if not available
612
+ #
613
+ # === Return
614
+ # true:: Always return true
615
+ def return_message(&blk)
616
+ each_usable do |b|
617
+ b.return_message do |to, reason, message|
618
+ context = @published.fetch(message)
619
+ context.record_failure(b.identity) if context
620
+ blk.call(b.identity, reason, message, to, context)
621
+ end
622
+ end
623
+ true
624
+ end
625
+
626
+ # Provide callback to be activated when a message cannot be delivered
627
+ #
628
+ # === Block
629
+ # Required block with parameters
630
+ # reason(String):: Non-delivery reason
631
+ # "NO_ROUTE" - queue does not exist
632
+ # "NO_CONSUMERS" - queue exists but it has no consumers, or if :immediate was specified,
633
+ # all consumers are not immediately ready to consume
634
+ # "ACCESS_REFUSED" - queue not usable because broker is in the process of stopping service
635
+ # type(String|nil):: Request type, or nil if not applicable
636
+ # token(String|nil):: Generated message identifier, or nil if not applicable
637
+ # from(String|nil):: Identity of original sender of message, or nil if not applicable
638
+ # to(String):: Queue to which message was published
639
+ #
640
+ # === Return
641
+ # true:: Always return true
642
+ def non_delivery(&blk)
643
+ @non_delivery = blk
644
+ true
645
+ end
646
+
647
+ # Delete queue in all usable brokers or all selected brokers that are usable
648
+ #
649
+ # === Parameters
650
+ # name(String):: Queue name
651
+ # options(Hash):: Queue declare options plus
652
+ # :brokers(Array):: Identity of brokers in which queue is to be deleted
653
+ #
654
+ # === Return
655
+ # identities(Array):: Identity of brokers where queue was deleted
656
+ def delete(name, options = {})
657
+ identities = []
658
+ u = usable
659
+ brokers = options.delete(:brokers)
660
+ ((brokers || u) & u).each { |i| identities << i if (b = @brokers_hash[i]) && b.delete(name, options) }
661
+ identities
662
+ end
663
+
664
+ # Delete queue resources from AMQP in all usable brokers
665
+ #
666
+ # === Parameters
667
+ # name(String):: Queue name
668
+ # options(Hash):: Queue declare options plus
669
+ # :brokers(Array):: Identity of brokers in which queue is to be deleted
670
+ #
671
+ # === Return
672
+ # identities(Array):: Identity of brokers where queue was deleted
673
+ def delete_amqp_resources(name, options = {})
674
+ identities = []
675
+ u = usable
676
+ ((options[:brokers] || u) & u).each { |i| identities << i if (b = @brokers_hash[i]) && b.delete_amqp_resources(:queue, name) }
677
+ identities
678
+ end
679
+
680
+ # Remove a broker client from the configuration
681
+ # Invoke connection status callbacks only if connection is not already disabled
682
+ # There is no check whether this is the last usable broker client
683
+ #
684
+ # === Parameters
685
+ # host{String):: IP host name or address for individual broker
686
+ # port(Integer):: TCP port number for individual broker
687
+ #
688
+ # === Block
689
+ # Optional block with following parameters to be called after removing the connection
690
+ # unless broker is not configured
691
+ # identity(String):: Broker serialized identity
692
+ #
693
+ # === Return
694
+ # identity(String|nil):: Serialized identity of broker removed, or nil if unknown
695
+ def remove(host, port, &blk)
696
+ identity = self.class.identity(host, port)
697
+ if broker = @brokers_hash[identity]
698
+ logger.info("Removing #{identity}, alias #{broker.alias} from broker list")
699
+ broker.close(propagate = true, normal = true, log = false)
700
+ @brokers_hash.delete(identity)
701
+ @brokers.reject! { |b| b.identity == identity }
702
+ yield identity if block_given?
703
+ else
704
+ logger.info("Ignored request to remove #{identity} from broker list because unknown")
705
+ identity = nil
706
+ end
707
+ identity
708
+ end
709
+
710
+ # Declare a broker client as unusable
711
+ #
712
+ # === Parameters
713
+ # identities(Array):: Identity of brokers
714
+ #
715
+ # === Return
716
+ # true:: Always return true
717
+ #
718
+ # === Raises
719
+ # Exception:: If identified broker is unknown
720
+ def declare_unusable(identities)
721
+ identities.each do |id|
722
+ broker = @brokers_hash[id]
723
+ raise Exception, "Cannot mark unknown broker #{id} unusable" unless broker
724
+ broker.close(propagate = true, normal = false, log = false)
725
+ end
726
+ end
727
+
728
+ # Close all broker client connections
729
+ #
730
+ # === Block
731
+ # Optional block with no parameters to be called after all connections are closed
732
+ #
733
+ # === Return
734
+ # true:: Always return true
735
+ def close(&blk)
736
+ if @closed
737
+ blk.call if blk
738
+ else
739
+ @closed = true
740
+ @connection_status = {}
741
+ handler = CountedDeferrable.new(@brokers.size)
742
+ handler.callback { blk.call if blk }
743
+ @brokers.each do |b|
744
+ begin
745
+ b.close(propagate = false) { handler.completed_one }
746
+ rescue Exception => e
747
+ handler.completed_one
748
+ logger.exception("Failed to close broker #{b.alias}", e, :trace)
749
+ @exceptions.track("close", e)
750
+ end
751
+ end
752
+ end
753
+ true
754
+ end
755
+
756
+ # Close an individual broker client connection
757
+ #
758
+ # === Parameters
759
+ # identity(String):: Broker serialized identity
760
+ # propagate(Boolean):: Whether to propagate connection status updates
761
+ #
762
+ # === Block
763
+ # Optional block with no parameters to be called after connection closed
764
+ #
765
+ # === Return
766
+ # true:: Always return true
767
+ #
768
+ # === Raise
769
+ # Exception:: If broker unknown
770
+ def close_one(identity, propagate = true, &blk)
771
+ broker = @brokers_hash[identity]
772
+ raise Exception, "Cannot close unknown broker #{identity}" unless broker
773
+ broker.close(propagate, &blk)
774
+ true
775
+ end
776
+
777
+ # Register callback to be activated when there is a change in connection status
778
+ # Can be called more than once without affecting previous callbacks
779
+ #
780
+ # === Parameters
781
+ # options(Hash):: Connection status monitoring options
782
+ # :one_off(Integer):: Seconds to wait for status change; only send update once;
783
+ # if timeout, report :timeout as the status
784
+ # :boundary(Symbol):: :any if only report change on any (0/1) boundary,
785
+ # :all if only report change on all (n-1/n) boundary, defaults to :any
786
+ # :brokers(Array):: Only report a status change for these identified brokers
787
+ #
788
+ # === Block
789
+ # Required block activated when connected count crosses a status boundary with following parameters
790
+ # status(Symbol):: Status of connection: :connected, :disconnected, or :failed, with
791
+ # :failed indicating that all selected brokers or all brokers have failed
792
+ #
793
+ # === Return
794
+ # id(String):: Identifier associated with connection status request
795
+ def connection_status(options = {}, &callback)
796
+ id = generate_id
797
+ @connection_status[id] = {:boundary => options[:boundary], :brokers => options[:brokers], :callback => callback}
798
+ if timeout = options[:one_off]
799
+ @connection_status[id][:timer] = EM::Timer.new(timeout) do
800
+ if @connection_status[id]
801
+ if @connection_status[id][:callback].arity == 2
802
+ @connection_status[id][:callback].call(:timeout, nil)
803
+ else
804
+ @connection_status[id][:callback].call(:timeout)
805
+ end
806
+ @connection_status.delete(id)
807
+ end
808
+ end
809
+ end
810
+ id
811
+ end
812
+
813
+ # Get status summary
814
+ #
815
+ # === Return
816
+ # (Array(Hash)):: Status of each configured broker with keys
817
+ # :identity(String):: Broker serialized identity
818
+ # :alias(String):: Broker alias used in logs
819
+ # :status(Symbol):: Status of connection
820
+ # :disconnects(Integer):: Number of times lost connection
821
+ # :failures(Integer):: Number of times connect failed
822
+ # :retries(Integer):: Number of attempts to connect after failure
823
+ def status
824
+ @brokers.map { |b| b.summary }
825
+ end
826
+
827
+ # Get broker client statistics
828
+ #
829
+ # === Parameters:
830
+ # reset(Boolean):: Whether to reset the statistics after getting the current ones
831
+ #
832
+ # === Return
833
+ # stats(Hash):: Broker client stats with keys
834
+ # "brokers"(Array):: Stats for each broker client in priority order
835
+ # "exceptions"(Hash|nil):: Exceptions raised per category, or nil if none
836
+ # "total"(Integer):: Total exceptions for this category
837
+ # "recent"(Array):: Most recent as a hash of "count", "type", "message", "when", and "where"
838
+ # "heartbeat"(Integer|nil):: Number of seconds between AMQP heartbeats, or nil if heartbeat disabled
839
+ # "returns"(Hash|nil):: Message return activity stats with keys "total", "percent", "last", and "rate"
840
+ # with percentage breakdown per return reason, or nil if none
841
+ def stats(reset = false)
842
+ stats = {
843
+ "brokers" => @brokers.map { |b| b.stats },
844
+ "exceptions" => @exceptions.stats,
845
+ "heartbeat" => @options[:heartbeat],
846
+ "returns" => @returns.all
847
+ }
848
+ reset_stats if reset
849
+ stats
850
+ end
851
+
852
+ # Reset broker client statistics
853
+ # Do not reset disconnect and failure stats because they might then be
854
+ # inconsistent with underlying connection status
855
+ #
856
+ # === Return
857
+ # true:: Always return true
858
+ def reset_stats
859
+ @returns = RightSupport::Stats::Activity.new
860
+ @exceptions = RightSupport::Stats::Exceptions.new(self, @options[:exception_callback])
861
+ true
862
+ end
863
+
864
+ protected
865
+
866
+ # Connect to all configured brokers
867
+ #
868
+ # === Return
869
+ # (Array):: Broker clients created
870
+ def connect_all
871
+ self.class.addresses(@options[:host], @options[:port]).map do |a|
872
+ identity = self.class.identity(a[:host], a[:port])
873
+ BrokerClient.new(identity, a, @serializer, @exceptions, @options, nil)
874
+ end
875
+ end
876
+
877
+ # Determine priority of broker
878
+ # If broker not found, assign next available priority
879
+ #
880
+ # === Parameters
881
+ # identity(String):: Broker identity
882
+ #
883
+ # === Return
884
+ # (Integer):: Priority position of broker
885
+ def priority(identity)
886
+ priority = 0
887
+ @brokers.each do |b|
888
+ break if b.identity == identity
889
+ priority += 1
890
+ end
891
+ priority
892
+ end
893
+
894
+ # Generate unique identity
895
+ #
896
+ # === Return
897
+ # (String):: Random 128-bit hexadecimal string
898
+ def generate_id
899
+ bytes = ''
900
+ 16.times { bytes << rand(0xff) }
901
+ # Transform into hex string
902
+ bytes.unpack('H*')[0]
903
+ end
904
+
905
+ # Iterate over clients that are usable, i.e., connecting or confirmed connected
906
+ #
907
+ # === Parameters
908
+ # identities(Array):: Identity of brokers to be considered, nil or empty array means all brokers
909
+ #
910
+ # === Block
911
+ # Optional block with following parameters to be called for each usable broker client
912
+ # broker(BrokerClient):: Broker client
913
+ #
914
+ # === Return
915
+ # (Array):: Usable broker clients
916
+ def each_usable(identities = nil)
917
+ choices = if identities && !identities.empty?
918
+ choices = identities.inject([]) { |c, i| if b = @brokers_hash[i] then c << b else c end }
919
+ else
920
+ @brokers
921
+ end
922
+ choices.select do |b|
923
+ if b.usable?
924
+ yield(b) if block_given?
925
+ true
926
+ end
927
+ end
928
+ end
929
+
930
+ # Select the broker clients to be used in the desired order
931
+ #
932
+ # === Parameters
933
+ # options(Hash):: Selection options:
934
+ # :brokers(Array):: Identity of brokers selected for use, defaults to all home brokers if nil or empty
935
+ # :order(Symbol):: Broker selection order: :random or :priority,
936
+ # defaults to @select if :brokers is nil, otherwise defaults to :priority
937
+ #
938
+ # === Return
939
+ # (Array):: Allowed BrokerClients in the order to be used
940
+ def use(options)
941
+ choices = []
942
+ select = options[:order]
943
+ if options[:brokers] && !options[:brokers].empty?
944
+ options[:brokers].each do |identity|
945
+ if choice = @brokers_hash[identity]
946
+ choices << choice
947
+ else
948
+ logger.exception("Invalid broker identity #{identity.inspect}, check server configuration")
949
+ end
950
+ end
951
+ else
952
+ choices = @brokers
953
+ select ||= @select
954
+ end
955
+ if select == :random
956
+ choices.sort_by { rand }
957
+ else
958
+ choices
959
+ end
960
+ end
961
+
962
+ # Callback from broker client with connection status update
963
+ # Makes client callback with :connected or :disconnected status if boundary crossed,
964
+ # or with :failed if all selected brokers or all brokers have failed
965
+ #
966
+ # === Parameters
967
+ # broker(BrokerClient):: Broker client reporting status update
968
+ # connected_before(Boolean):: Whether client was connected before this update
969
+ #
970
+ # === Return
971
+ # true:: Always return true
972
+ def update_status(broker, connected_before)
973
+ after = connected
974
+ before = after.clone
975
+ before.delete(broker.identity) if broker.connected? && !connected_before
976
+ before.push(broker.identity) if !broker.connected? && connected_before
977
+ unless before == after
978
+ logger.info("[status] Broker #{broker.alias} is now #{broker.status}, " +
979
+ "connected brokers: [#{aliases(after).join(", ")}]")
980
+ end
981
+ @connection_status.reject! do |k, v|
982
+ reject = false
983
+ if v[:brokers].nil? || v[:brokers].include?(broker.identity)
984
+ b, a, n, f = if v[:brokers].nil?
985
+ [before, after, @brokers.size, all]
986
+ else
987
+ [before & v[:brokers], after & v[:brokers], v[:brokers].size, v[:brokers]]
988
+ end
989
+ update = if v[:boundary] == :all
990
+ if b.size < n && a.size == n
991
+ :connected
992
+ elsif b.size == n && a.size < n
993
+ :disconnected
994
+ elsif (f - failed).empty?
995
+ :failed
996
+ end
997
+ else
998
+ if b.size == 0 && a.size > 0
999
+ :connected
1000
+ elsif b.size > 0 && a.size == 0
1001
+ :disconnected
1002
+ elsif (f - failed).empty?
1003
+ :failed
1004
+ end
1005
+ end
1006
+ if update
1007
+ v[:callback].call(update)
1008
+ if v[:timer]
1009
+ v[:timer].cancel
1010
+ reject = true
1011
+ end
1012
+ end
1013
+ end
1014
+ reject
1015
+ end
1016
+ true
1017
+ end
1018
+
1019
+ # Handle message returned by broker because it could not deliver it
1020
+ # If agent still active, resend using another broker
1021
+ # If this is last usable broker and persistent is enabled, allow message to be queued
1022
+ # on next send even if the queue has no consumers so there is a chance of message
1023
+ # eventually being delivered
1024
+ # If persistent or one-way request and all usable brokers have failed, try one more time
1025
+ # without mandatory flag to give message opportunity to be queued
1026
+ # If there are no more usable broker clients, send non-delivery message to original sender
1027
+ #
1028
+ # === Parameters
1029
+ # identity(String):: Identity of broker that could not deliver message
1030
+ # reason(String):: Reason for return
1031
+ # "NO_ROUTE" - queue does not exist
1032
+ # "NO_CONSUMERS" - queue exists but it has no consumers, or if :immediate was specified,
1033
+ # all consumers are not immediately ready to consume
1034
+ # "ACCESS_REFUSED" - queue not usable because broker is in the process of stopping service
1035
+ # message(String):: Returned message in serialized packet format
1036
+ # to(String):: Queue to which message was published
1037
+ # context(Context):: Message publishing context
1038
+ #
1039
+ # === Return
1040
+ # true:: Always return true
1041
+ def handle_return(identity, reason, message, to, context)
1042
+ @brokers_hash[identity].update_status(:stopping) if reason == "ACCESS_REFUSED"
1043
+
1044
+ if context
1045
+ @returns.update("#{alias_(identity)} (#{reason.to_s.downcase})")
1046
+ name = context.name
1047
+ options = context.options || {}
1048
+ token = context.token
1049
+ one_way = context.one_way
1050
+ persistent = options[:persistent]
1051
+ mandatory = true
1052
+ remaining = (context.brokers - context.failed) & connected
1053
+ logger.info("RETURN reason #{reason} token <#{token}> to #{to} from #{context.from} brokers #{context.brokers.inspect} " +
1054
+ "failed #{context.failed.inspect} remaining #{remaining.inspect} connected #{connected.inspect}")
1055
+ if remaining.empty?
1056
+ if (persistent || one_way) &&
1057
+ ["ACCESS_REFUSED", "NO_CONSUMERS"].include?(reason) &&
1058
+ !(remaining = context.brokers & connected).empty?
1059
+ # Retry because persistent, and this time w/o mandatory so that gets queued even though no consumers
1060
+ mandatory = false
1061
+ else
1062
+ t = token ? " <#{token}>" : ""
1063
+ logger.info("NO ROUTE #{aliases(context.brokers).join(", ")} [#{name}]#{t} to #{to}")
1064
+ @non_delivery.call(reason, context.type, token, context.from, to) if @non_delivery
1065
+ end
1066
+ end
1067
+
1068
+ unless remaining.empty?
1069
+ t = token ? " <#{token}>" : ""
1070
+ p = persistent ? ", persistent" : ""
1071
+ m = mandatory ? ", mandatory" : ""
1072
+ logger.info("RE-ROUTE #{aliases(remaining).join(", ")} [#{context.name}]#{t} to #{to}#{p}#{m}")
1073
+ exchange = {:type => :queue, :name => to, :options => {:no_declare => true}}
1074
+ publish(exchange, message, options.merge(:no_serialize => true, :brokers => remaining,
1075
+ :persistent => persistent, :mandatory => mandatory))
1076
+ end
1077
+ else
1078
+ @returns.update("#{alias_(identity)} (#{reason.to_s.downcase} - missing context)")
1079
+ logger.info("Dropping message returned from broker #{identity} for reason #{reason} " +
1080
+ "because no message context available for re-routing it to #{to}")
1081
+ end
1082
+ true
1083
+ rescue Exception => e
1084
+ logger.exception("Failed to handle #{reason} return from #{identity} for message being routed to #{to}", e, :trace)
1085
+ @exceptions.track("return", e)
1086
+ end
1087
+
1088
+ # Helper for deferring block execution until specified number of actions have completed
1089
+ # or timeout occurs
1090
+ class CountedDeferrable
1091
+
1092
+ include EM::Deferrable
1093
+
1094
+ # Defer action until completion count reached or timeout occurs
1095
+ #
1096
+ # === Parameter
1097
+ # count(Integer):: Number of completions required for action
1098
+ # timeout(Integer|nil):: Number of seconds to wait for all completions and if
1099
+ # reached, proceed with action; nil means no timing
1100
+ def initialize(count, timeout = nil)
1101
+ @timer = EM::Timer.new(timeout) { succeed } if timeout
1102
+ @count = count
1103
+ end
1104
+
1105
+ # Completed one part of task
1106
+ #
1107
+ # === Return
1108
+ # true:: Always return true
1109
+ def completed_one
1110
+ if (@count -= 1) == 0
1111
+ @timer.cancel if @timer
1112
+ succeed
1113
+ end
1114
+ true
1115
+ end
1116
+
1117
+ end # CountedDeferrable
1118
+
1119
+ # Cache for context of recently published messages for use with message returns
1120
+ # Applies LRU for managing cache size but only deletes entries when old enough
1121
+ class Published
1122
+
1123
+ # Number of seconds since a cache entry was last used before it is deleted
1124
+ MAX_AGE = 60
1125
+
1126
+ # Initialize cache
1127
+ def initialize
1128
+ @cache = {}
1129
+ @lru = []
1130
+ end
1131
+
1132
+ # Store message context in cache
1133
+ #
1134
+ # === Parameters
1135
+ # message(String):: Serialized message that was published
1136
+ # context(Context):: Message publishing context
1137
+ #
1138
+ # === Return
1139
+ # true:: Always return true
1140
+ def store(message, context)
1141
+ key = identify(message)
1142
+ now = Time.now.to_i
1143
+ if entry = @cache[key]
1144
+ entry[0] = now
1145
+ @lru.push(@lru.delete(key))
1146
+ else
1147
+ @cache[key] = [now, context]
1148
+ @lru.push(key)
1149
+ @cache.delete(@lru.shift) while (now - @cache[@lru.first][0]) > MAX_AGE
1150
+ end
1151
+ true
1152
+ end
1153
+
1154
+ # Fetch context of previously published message
1155
+ #
1156
+ # === Parameters
1157
+ # message(String):: Serialized message that was published
1158
+ #
1159
+ # === Return
1160
+ # (Context|nil):: Context of message, or nil if not found in cache
1161
+ def fetch(message)
1162
+ key = identify(message)
1163
+ if entry = @cache[key]
1164
+ entry[0] = Time.now.to_i
1165
+ @lru.push(@lru.delete(key))
1166
+ entry[1]
1167
+ end
1168
+ end
1169
+
1170
+ # Obtain a unique identifier for this message
1171
+ #
1172
+ # === Parameters
1173
+ # message(String):: Serialized message that was published
1174
+ #
1175
+ # === Returns
1176
+ # (String):: Unique id for message
1177
+ def identify(message)
1178
+ Digest::MD5.hexdigest(message)
1179
+ end
1180
+
1181
+ end # Published
1182
+
1183
+ end # HABrokerClient
1184
+
1185
+ end # RightAMQP