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,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