right_amqp 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/LICENSE +20 -0
- data/README.rdoc +216 -0
- data/Rakefile +70 -0
- data/lib/right_amqp.rb +28 -0
- data/lib/right_amqp/amqp.rb +115 -0
- data/lib/right_amqp/amqp/buffer.rb +395 -0
- data/lib/right_amqp/amqp/client.rb +282 -0
- data/lib/right_amqp/amqp/frame.rb +124 -0
- data/lib/right_amqp/amqp/protocol.rb +212 -0
- data/lib/right_amqp/amqp/server.rb +99 -0
- data/lib/right_amqp/amqp/spec.rb +832 -0
- data/lib/right_amqp/amqp/version.rb +3 -0
- data/lib/right_amqp/ext/blankslate.rb +7 -0
- data/lib/right_amqp/ext/em.rb +8 -0
- data/lib/right_amqp/ext/emfork.rb +69 -0
- data/lib/right_amqp/ha_client.rb +25 -0
- data/lib/right_amqp/ha_client/broker_client.rb +690 -0
- data/lib/right_amqp/ha_client/ha_broker_client.rb +1185 -0
- data/lib/right_amqp/mq.rb +866 -0
- data/lib/right_amqp/mq/exchange.rb +304 -0
- data/lib/right_amqp/mq/header.rb +33 -0
- data/lib/right_amqp/mq/logger.rb +89 -0
- data/lib/right_amqp/mq/queue.rb +456 -0
- data/lib/right_amqp/mq/rpc.rb +100 -0
- data/right_amqp.gemspec +57 -0
- data/spec/amqp/client_reconnect_spec.rb +105 -0
- data/spec/ha_client/broker_client_spec.rb +936 -0
- data/spec/ha_client/ha_broker_client_spec.rb +1385 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +56 -0
- metadata +141 -0
@@ -0,0 +1,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
|