mbus 1.0.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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/README.mediawiki +169 -0
  3. data/Rakefile +24 -0
  4. data/bin/console +11 -0
  5. data/bin/messagebus_swarm +77 -0
  6. data/lib/messagebus.rb +62 -0
  7. data/lib/messagebus/client.rb +166 -0
  8. data/lib/messagebus/cluster_map.rb +161 -0
  9. data/lib/messagebus/connection.rb +118 -0
  10. data/lib/messagebus/consumer.rb +447 -0
  11. data/lib/messagebus/custom_errors.rb +37 -0
  12. data/lib/messagebus/dottable_hash.rb +113 -0
  13. data/lib/messagebus/error_status.rb +42 -0
  14. data/lib/messagebus/logger.rb +45 -0
  15. data/lib/messagebus/message.rb +168 -0
  16. data/lib/messagebus/messagebus_types.rb +107 -0
  17. data/lib/messagebus/producer.rb +187 -0
  18. data/lib/messagebus/swarm.rb +49 -0
  19. data/lib/messagebus/swarm/controller.rb +296 -0
  20. data/lib/messagebus/swarm/drone.rb +195 -0
  21. data/lib/messagebus/swarm/drone/logging_worker.rb +53 -0
  22. data/lib/messagebus/validations.rb +68 -0
  23. data/lib/messagebus/version.rb +36 -0
  24. data/messagebus.gemspec +29 -0
  25. data/spec/messagebus/client_spec.rb +157 -0
  26. data/spec/messagebus/cluster_map_spec.rb +178 -0
  27. data/spec/messagebus/consumer_spec.rb +338 -0
  28. data/spec/messagebus/dottable_hash_spec.rb +137 -0
  29. data/spec/messagebus/message_spec.rb +93 -0
  30. data/spec/messagebus/producer_spec.rb +147 -0
  31. data/spec/messagebus/swarm/controller_spec.rb +73 -0
  32. data/spec/messagebus/validations_spec.rb +71 -0
  33. data/spec/spec_helper.rb +10 -0
  34. data/vendor/gems/stomp.rb +23 -0
  35. data/vendor/gems/stomp/client.rb +360 -0
  36. data/vendor/gems/stomp/connection.rb +583 -0
  37. data/vendor/gems/stomp/errors.rb +39 -0
  38. data/vendor/gems/stomp/ext/hash.rb +24 -0
  39. data/vendor/gems/stomp/message.rb +68 -0
  40. metadata +138 -0
@@ -0,0 +1,161 @@
1
+ # Copyright (c) 2012, Groupon, Inc.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions
6
+ # are met:
7
+ #
8
+ # Redistributions of source code must retain the above copyright notice,
9
+ # this list of conditions and the following disclaimer.
10
+ #
11
+ # Redistributions in binary form must reproduce the above copyright
12
+ # notice, this list of conditions and the following disclaimer in the
13
+ # documentation and/or other materials provided with the distribution.
14
+ #
15
+ # Neither the name of GROUPON nor the names of its contributors may be
16
+ # used to endorse or promote products derived from this software without
17
+ # specific prior written permission.
18
+ #
19
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
20
+ # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
21
+ # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
22
+ # PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23
+ # HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
25
+ # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
26
+ # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
27
+ # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
28
+ # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
29
+ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+
31
+ require 'messagebus/dottable_hash'
32
+
33
+ module Messagebus
34
+ class ClusterMap
35
+ attr_reader :address, :destinations
36
+
37
+ include Validations
38
+ def initialize(config)
39
+ config = DottableHash.new(config)
40
+ Messagebus::Client.logger.debug { "Initializing ClusterMap with config: #{config.inspect}" }
41
+
42
+ if clusters = config.clusters
43
+ config.clusters.each do |cluster_config|
44
+ # Merge cluster config with top level config.
45
+ # cluster level values should override top level values.
46
+ cluster = config.merge(cluster_config)
47
+ create_cluster(cluster)
48
+ end
49
+ end
50
+ end
51
+
52
+ def start
53
+ cluster_producer_map.each do |cluster_name, producer|
54
+ Messagebus::Client.logger.info "Starting producer for cluster: #{cluster_name} with host_params: #{producer.host_params}"
55
+ producer.start
56
+ end
57
+ end
58
+
59
+ def stop
60
+ cluster_producer_map.each do |cluster_name, producer|
61
+ Messagebus::Client.logger.info "Stopping producer for cluster: #{cluster_name} with host_params: #{producer.host_params}"
62
+ if producer.started?
63
+ producer.stop
64
+ else
65
+ Messagebus::Client.logger.warn "#{producer.host_params} was not active, ignoring stop request."
66
+ end
67
+ end
68
+ end
69
+
70
+ def find(destination_name)
71
+ destinations[destination_name]
72
+ end
73
+
74
+ def destinations
75
+ @destinations ||= {}
76
+ end
77
+
78
+ def update_config(config)
79
+ Messagebus::Client.logger.debug { "Reloading ClusterMap with config: #{config.inspect}" }
80
+ config = DottableHash.new(config)
81
+ if clusters = config.clusters
82
+ config.clusters.each do |cluster_config|
83
+ cluster = config.merge(cluster_config)
84
+ #cluster exists - check and update configs
85
+ if cluster_producer_map.has_key?(cluster.name)
86
+ #check for prodcuer config
87
+ update_cluster(cluster)
88
+ else
89
+ #new cluster => create it
90
+ create_cluster(cluster, true)
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ private
97
+
98
+ def update_cluster(cluster)
99
+ producer = cluster_producer_map[cluster.name]
100
+ #check for new producer address =>add to exisiting host params list
101
+ #do nothing if producer not found in new config
102
+ cluster_host_params = [cluster.producer_address] unless cluster.producer_address.is_a?(Array)
103
+ producer_host_params = producer.host_params
104
+ cluster_host_params.each do |address|
105
+ if !producer_host_params.include?(address)
106
+ producer_host_params = producer_host_params.to_a.push address
107
+ end
108
+ end
109
+ producer.host_params=(producer_host_params)
110
+
111
+ options = producer.options
112
+ options['receipt_wait_timeout_ms'] = cluster.receipt_wait_timeout_ms || options['receipt_wait_timeout_ms']
113
+ options['conn_lifetime_sec'] = cluster.conn_lifetime_sec || options['conn_lifetime_sec']
114
+
115
+ producer.options=(options)
116
+
117
+ #load new destination, same producer reference used
118
+ if cluster.destinations && !cluster.destinations.empty?
119
+ cluster.destinations.each do |destination_name|
120
+ load_destination(destination_name, producer)
121
+ end
122
+ else
123
+ raise Client::InitializationError.new("no destinations defined")
124
+ end
125
+ end
126
+
127
+ def create_cluster(cluster, producer_start = false)
128
+ Messagebus::Client.logger.debug "Initializing cluster: #{cluster.inspect}"
129
+
130
+ producer = Messagebus::Producer.new(
131
+ cluster.producer_address,
132
+ :user => cluster.user,
133
+ :passwd => cluster.passwd,
134
+ :receipt_wait_timeout_ms => cluster.receipt_wait_timeout_ms || 5000,
135
+ :conn_lifetime_sec => cluster.conn_lifetime_sec || 300
136
+ )
137
+ cluster_producer_map[cluster.name] = producer
138
+
139
+ if cluster.destinations && !cluster.destinations.empty?
140
+ cluster.destinations.each do |destination_name|
141
+ load_destination(destination_name, producer)
142
+ end
143
+ else
144
+ raise Client::InitializationError.new("no destinations defined")
145
+ end
146
+ if producer_start
147
+ producer.start
148
+ end
149
+ end
150
+
151
+ def cluster_producer_map
152
+ @cluster_producer_map ||= {}
153
+ end
154
+
155
+ def load_destination(destination_name, producer)
156
+ validate_destination_config(destination_name)
157
+ destinations[destination_name] = producer
158
+ Messagebus::Client.logger.info "loaded #{destination_name} => #{producer.host_params}"
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,118 @@
1
+ # Copyright (c) 2012, Groupon, Inc.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions
6
+ # are met:
7
+ #
8
+ # Redistributions of source code must retain the above copyright notice,
9
+ # this list of conditions and the following disclaimer.
10
+ #
11
+ # Redistributions in binary form must reproduce the above copyright
12
+ # notice, this list of conditions and the following disclaimer in the
13
+ # documentation and/or other materials provided with the distribution.
14
+ #
15
+ # Neither the name of GROUPON nor the names of its contributors may be
16
+ # used to endorse or promote products derived from this software without
17
+ # specific prior written permission.
18
+ #
19
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
20
+ # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
21
+ # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
22
+ # PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23
+ # HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
25
+ # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
26
+ # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
27
+ # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
28
+ # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
29
+ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+
31
+ require "messagebus/validations"
32
+ require "messagebus/dottable_hash"
33
+
34
+ module Messagebus
35
+ class Connection
36
+
37
+ STARTED = "STARTED"
38
+ STOPPED = "STOPPED"
39
+
40
+ attr_accessor :host_params, :options
41
+
42
+ include Validations
43
+
44
+ def initialize(host_params, passed_options = {})
45
+ @host_params = host_params
46
+ @host_params = [@host_params] unless @host_params.is_a?(Array)
47
+
48
+ @options = DottableHash.new({
49
+ :user => '', :passwd => '',
50
+ :conn_lifetime_sec => 300, :receipt_wait_timeout_ms => 5000,
51
+ :destination_name => nil, :destination_type => nil,
52
+ :ack_type => Messagebus::ACK_TYPE_AUTO_CLIENT, :num_threads_per_server => 1,
53
+ :enable_dynamic_serverlist_fetch => false, :dynamic_fetch_timeout_ms => 1000,
54
+ :dynamic_serverlist_fetch_url_override => nil
55
+ }).merge(passed_options)
56
+
57
+ @state = STOPPED
58
+ end
59
+
60
+ def started?
61
+ @state == STARTED
62
+ end
63
+
64
+ def stopped?
65
+ @state == STOPPED
66
+ end
67
+
68
+ def do_with_timeout(timeout_ms)
69
+ if not block_given?
70
+ raise "do_with_timeout expects a block to be run"
71
+ end
72
+
73
+ start_time = Time.now
74
+ while (Time.now - start_time) * 1000 < timeout_ms
75
+ yield
76
+ end
77
+ end
78
+
79
+ def start_server(host_params, user, passwd, subscription_id=nil)
80
+ case host_params
81
+ when Array
82
+ host_param = host_params[rand(host_params.length)]
83
+ when String
84
+ host_param = host_params
85
+ end
86
+
87
+ host, port = host_param.split(':')
88
+
89
+ connect_headers = {}
90
+ connect_headers.merge!("client-id" => subscription_id) if subscription_id
91
+
92
+ stomp = Stomp::Client.new(user, passwd, host, port, logger, connect_headers)
93
+ logger.info "Started client for host_param:#{host_param} stomp-client:#{stomp} user:#{user}"
94
+ @state = STARTED
95
+
96
+ return stomp
97
+ end
98
+
99
+ def stop_server(stomp)
100
+ Client.logger.info "Stopping stomp-client:#{stomp}"
101
+ stomp.close if stomp
102
+ @state = STOPPED
103
+ end
104
+
105
+ def host_params=(host_params)
106
+ @host_params = host_params
107
+ end
108
+
109
+ def options=(options)
110
+ @options = options
111
+ end
112
+ private
113
+
114
+ def logger
115
+ @logger ||= Client.logger
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,447 @@
1
+ # Copyright (c) 2012, Groupon, Inc.
2
+ # All rights reserved.
3
+ #
4
+ # Redistribution and use in source and binary forms, with or without
5
+ # modification, are permitted provided that the following conditions
6
+ # are met:
7
+ #
8
+ # Redistributions of source code must retain the above copyright notice,
9
+ # this list of conditions and the following disclaimer.
10
+ #
11
+ # Redistributions in binary form must reproduce the above copyright
12
+ # notice, this list of conditions and the following disclaimer in the
13
+ # documentation and/or other materials provided with the distribution.
14
+ #
15
+ # Neither the name of GROUPON nor the names of its contributors may be
16
+ # used to endorse or promote products derived from this software without
17
+ # specific prior written permission.
18
+ #
19
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
20
+ # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
21
+ # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
22
+ # PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23
+ # HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
25
+ # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
26
+ # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
27
+ # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
28
+ # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
29
+ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+
31
+ require 'thread'
32
+ require 'net/http'
33
+ require "messagebus/dottable_hash"
34
+
35
+ module Messagebus
36
+ # Consumer client class. Provides a single access thread for all messagebus servers.
37
+ # Takes in a list of messagebus servers to receive from, open connections to
38
+ # all servers, does round robin receives across all servers.
39
+ #
40
+ # parameters:
41
+ # dest (String, required value, name of the queue/topic)
42
+ # host_params (list<string>, required value, eg. '[localhost:61613]')
43
+ # options : A hash map for optional values.
44
+ # user (String, default : '')
45
+ # passwd (String, default : '')
46
+ # ack_type (String, required value: Messagebus::ACK_TYPE_AUTO_CLIENT OR Messagebus::ACK_TYPE_CLIENT)
47
+ # autoClient: module acks internally automatically for each receive.
48
+ # client: User should explicit ack *each* message.
49
+ # conn_lifetime_sec (Int, default:300 secs)
50
+ # subscription_id (String, required for topic, Each subscription is identified by a unique Id,
51
+ # for a topic different subscriptions means each subscription gets copy of
52
+ # message each, same subscription_id across multiple Consumers means load-balancing
53
+ # messages for that subscription.)
54
+ # enable_dynamic_serverlist_fetch: (Boolean, Enable the consumer to fetch the list of brokers actively default: true)
55
+ # dynamic_serverlist_fetch_url_override (String, The override url to fetch the list of consumers dynamically.)
56
+ # dynamic_fetch_timeout_ms (Integer, milliseconds to wait for http response of dynamic serverlist fetch)
57
+ # receipt_wait_timeout_ms (Int, optoional value, default: 5 seconds)
58
+
59
+ class Consumer < Connection
60
+ attr_accessor :received_messages, :servers_running, :state
61
+
62
+ def self.start(host_params, options={})
63
+ consumer = new(host_params, options)
64
+ consumer.start
65
+ if block_given?
66
+ begin
67
+ yield consumer
68
+ ensure
69
+ consumer.stop
70
+ end
71
+ end
72
+ consumer
73
+ end
74
+
75
+ def initialize(host_params, options = {})
76
+ options = DottableHash.new({:ack_type => Messagebus::ACK_TYPE_CLIENT, :enable_dynamic_serverlist_fetch => true }).merge(options)
77
+ options.merge!(options.cluster_defaults) if options.cluster_defaults
78
+
79
+ super(host_params, options)
80
+
81
+ validate_destination_config(@options.destination_name, true, options)
82
+ validate_connection_config(@host_params, options)
83
+
84
+ @received_messages = Queue.new
85
+ @servers_running = {}
86
+ @logger = Logger.new(options[:log_file]) if options[:log_file]
87
+ end
88
+
89
+ def logger
90
+ @logger ||= Client.logger
91
+ end
92
+
93
+ # Start the consumers and all connections.
94
+ # Optionally takes a block to which it yields self. When the block is
95
+ # passed, it will auto close the connections after the block finishes.
96
+ def start
97
+ @state = STARTED
98
+ logger.info("Starting consumers with host_params:#{@host_params.inspect} for destination:#{@options.destination_name}")
99
+ start_servers(@host_params, true)
100
+ refresh_servers
101
+ end
102
+
103
+ # Close the consumers and all connections
104
+ def stop
105
+ @state = STOPPED
106
+ logger.info("Stopping consumers for running servers:#{@servers_running.keys.inspect}")
107
+
108
+ stop_servers(@servers_running.keys)
109
+ end
110
+
111
+ ##
112
+ # This is used to insert an unblock message into the consumer. A use case
113
+ # is when you're using a blocking receive, and you want to unblock a
114
+ # separate thread or tell a consumer to unblock from a signal handler.
115
+ # See also Messagebus::Swarm::Drone#stop
116
+ #
117
+ # http://en.wikipedia.org/wiki/Sentinel_value
118
+ def insert_sentinel_value(final_message=nil)
119
+ # push a message onto our consumer so that if we're currently blocking on waiting for a message
120
+ # we'll see this and do no further processing
121
+ @received_messages.push({:stop_processing_sentinel => true, :msg => final_message})
122
+ end
123
+
124
+ # Blocking receive: block till a value is available.
125
+ # Returns the message(Messagebus::Message) received or block indefinately.
126
+ def receive
127
+ return receive_internal(non_blocking=false)
128
+ end
129
+
130
+ # Blocking receive with timeout: block till a value is available for passed timeout.
131
+ # Returns the message(Messagebus::Message) received or raise MessageReceiveTimeout("timeout")
132
+ def receive_timeout(timeout_ms=1000)
133
+ do_with_timeout(timeout_ms) {
134
+ if @received_messages.empty?
135
+ sleep 0.01
136
+ else
137
+ return receive_internal(non_blocking=true)
138
+ end
139
+ }
140
+
141
+ raise MessageReceiveTimeout, "receive timeout(" + timeout_ms.to_s + ") while waiting for message to arrive."
142
+ end
143
+
144
+ # Non-Blocking receive.
145
+ # Returns the message(Messagebus::Message) received or nil immediately.
146
+ def receive_immediate()
147
+ if not @received_messages.empty?
148
+ return receive_internal(non_blocking=true)
149
+ else
150
+ return nil
151
+ end
152
+ end
153
+
154
+ # Send consumer credit back for last received message.
155
+ def credit()
156
+ if not @last_received.nil?
157
+ begin
158
+ logger.info("Sending consumer credit for message with id:#{@last_received[:decoded_msg].message_id}")
159
+
160
+ @servers_running[@last_received[:host_param]].credit(@last_received[:msg])
161
+ rescue NameError => e
162
+ logger.error("Failed to credit message. Was the connection removed?. #{e.message} #{e.backtrace.join("|")}")
163
+ end
164
+ end
165
+ end
166
+
167
+ # Ack the last received message.
168
+ # Message broker will keep resending messages (after retry_wait and upto retry_max_times) till
169
+ # it sees an ack for the message.
170
+ def ack(safe_mode = false)
171
+ if not @last_received.nil?
172
+ begin
173
+ logger.info("Sending ack() for message with id:#{@last_received[:decoded_msg].message_id}")
174
+ if not safe_mode
175
+ begin
176
+ @servers_running[@last_received[:host_param]].acknowledge(@last_received[:msg])
177
+ @last_received = nil
178
+ return true
179
+
180
+ rescue => e
181
+ logger.error("Failed to ack message. Was the connection removed? #{e.message} #{e.backtrace.join("|")}")
182
+ end
183
+ else
184
+ receipt_received = false
185
+ errors_received = nil
186
+ @servers_running[@last_received[:host_param]].acknowledge(@last_received[:msg]) do |msg|
187
+ if msg.command == 'ERROR'
188
+ errors_received = msg
189
+ raise "Failed to ack message with Error: #{msg.body.to_s} #{caller}"
190
+ else
191
+ receipt_received = true
192
+ @last_received = nil
193
+ end
194
+ end
195
+
196
+ # wait for receipt up to given timeout.
197
+ do_with_timeout(@options.receipt_wait_timeout_ms) do
198
+ if errors_received
199
+ raise "Failed to ack message in safe mode with Error: " + errors_received.body.to_s
200
+ end
201
+
202
+ if not receipt_received
203
+ sleep 0.005
204
+ else
205
+ return true
206
+ end
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
212
+
213
+ def nack
214
+ if not @last_received.nil?
215
+ begin
216
+ logger.info("Sending nack() for message with id:#{@last_received[:decoded_msg].message_id}")
217
+
218
+ @servers_running[@last_received[:host_param]].nack(@last_received[:msg])
219
+ @last_received = nil
220
+ rescue => e
221
+ logger.error("Failed to nack message. Was the connection removed? #{e.message} #{e.backtrace.join("|")}")
222
+ end
223
+ end
224
+ end
225
+
226
+ def keepalive
227
+ @servers_running.each do |host_param, client|
228
+ begin
229
+ client.keepalive()
230
+ @last_received = nil
231
+ rescue => e
232
+ logger.error("Failed to send keepalive to #{host_param}")
233
+ end
234
+ end
235
+ end
236
+
237
+
238
+ def fetch_serverlist
239
+ if @options.dynamic_serverlist_fetch_url_override
240
+ dynamic_serverlist_fetch_url = @options.dynamic_serverlist_fetch_url_override
241
+ else
242
+ dynamic_serverlist_fetch_url = get_dynamic_fetch_url(@host_params)
243
+ end
244
+
245
+ logger.info("trying to fetch dynamic url #{dynamic_serverlist_fetch_url}")
246
+ begin
247
+ data = fetch_uri(dynamic_serverlist_fetch_url)
248
+ data = data.gsub(' ', '')
249
+ serverlist = data.split(',')
250
+ serverlist.each do |server|
251
+ if SERVER_REGEX.match(server).nil?
252
+ raise "bad data returned from dynamic url: #{data}"
253
+ end
254
+ end
255
+ return serverlist
256
+
257
+ rescue => e
258
+ logger.error("Failed to fetch server list from url:#{dynamic_serverlist_fetch_url} with exception: #{e.message}, #{e.backtrace.join("|")}")
259
+ return nil
260
+ end
261
+ end
262
+
263
+ def get_dynamic_fetch_url(host_params)
264
+ case host_params
265
+ when Array
266
+ host_param = host_params[rand(host_params.length)]
267
+ when String
268
+ host_param = host_params
269
+ end
270
+
271
+ host, port = host_param.split(':')
272
+ return 'http://' + host + ':8081/jmx?command=get_attribute&args=org.hornetq%3Amodule%3DCore%2Ctype%3DServer%20ListOfBrokers'
273
+ end
274
+
275
+ def delete_subscription()
276
+ host_params = @servers_running.keys
277
+ if not host_params.nil?
278
+ host_params.each do |host_param|
279
+ logger.info("Unsubscribing #{@options.destination_name} consumer client for #{host_param}")
280
+ client = @servers_running[host_param]
281
+ client.unsubscribe(@options.destination_name)
282
+ end
283
+ end
284
+ end
285
+
286
+ def refresh_servers
287
+ logger.info("refreshing consumer threads.")
288
+ @refresh_time = Time.new()
289
+ hosts = @servers_running.keys
290
+ if @options.enable_dynamic_serverlist_fetch
291
+ # Fetch the server list from the dynamic server list fetch url.
292
+ begin
293
+ updated_server_list = fetch_serverlist
294
+ rescue => e
295
+ logger.error "Error in refresh server #{e} \n Stack Trace: #{e.backtrace.join("|")}"
296
+ end
297
+
298
+ if not updated_server_list.nil?
299
+ hosts = updated_server_list
300
+ end
301
+ end
302
+
303
+ logger.info("refreshing servers current_list:#{@servers_running.keys.inspect} new list:#{hosts.inspect}")
304
+
305
+ servers_added = hosts - @servers_running.keys
306
+
307
+ # start new servers
308
+ if servers_added and not servers_added.empty?
309
+ logger.info("Adding new servers in:#{servers_added.inspect}")
310
+ if not servers_added.empty?()
311
+ start_servers(servers_added)
312
+ end
313
+ end
314
+
315
+ end
316
+
317
+ private
318
+ def fetch_uri(uri)
319
+ uri = URI(uri)
320
+ begin
321
+ if uri and http = Net::HTTP.new(uri.host, uri.port)
322
+ http.open_timeout = @options.dynamic_fetch_timeout_ms/1000.0
323
+ http.read_timeout = @options.dynamic_fetch_timeout_ms/1000.0
324
+ http.start {|http|
325
+ response = http.request_get(uri.request_uri)
326
+ if response.class == Net::HTTPOK
327
+ logger.info("fetched_uri: #{response.body}")
328
+ return response.body
329
+ else
330
+ logger.error("fetch_uri got bad response from server:#{response}")
331
+ return nil
332
+ end
333
+ }
334
+ end
335
+ rescue => e
336
+ logger.error("Failed to fetch dynamic server list with exception: #{e.message}, #{e.backtrace.join("|")}")
337
+ return nil
338
+ end
339
+ end
340
+
341
+
342
+ def subscribe_client(client)
343
+ # Add the options for the client.
344
+ # ack is 'client' for server in both 'client'/'autoClient' cases.
345
+ options = {:ack => "client"}
346
+ if not @options.subscription_id.nil?
347
+ options["durable-subscriber-name"] = @options.subscription_id
348
+ options["id"] = @options.subscription_id
349
+ options["client-id"] = @options.subscription_id
350
+ else
351
+ # We need to set 'id' irrespective here.
352
+ options[:id] = @options.destination_name
353
+ end
354
+
355
+ client.subscribe(@options.destination_name, options) do |msg|
356
+ decoded_msg = nil
357
+ if msg.command != 'ERROR'
358
+ begin
359
+ decoded_msg = Messagebus::Message.get_message_from_thrift_binary(msg.body)
360
+ decoded_msg.message_properties = msg.headers
361
+ @received_messages.push({:msg => msg, :host_param => client.host + ":" + client.port.to_s, :decoded_msg => decoded_msg})
362
+ rescue => e
363
+ logger.error("Failed to decode message:\n#{msg}\n#{e.message} #{e.backtrace.join("|")}")
364
+ end
365
+ else
366
+ logger.info("ERROR frame received:\n#{msg}" )
367
+ end
368
+
369
+ end
370
+ end
371
+
372
+ def start_servers(host_params, fail_on_error = false)
373
+ host_params = [host_params] if host_params.is_a?(String)
374
+
375
+ if host_params
376
+ host_params.each do |host_param|
377
+ begin
378
+ logger.info("Starting messagebus consumer client for #{host_param}")
379
+
380
+ client = start_server(host_param, @options.user, @options.passwd, @options.subscription_id)
381
+ subscribe_client(client)
382
+ @servers_running[host_param] = client
383
+ rescue => e
384
+ logger.error("Failed to start server #{host_param} with exception: #{e.message}, #{e.backtrace.join("|")}")
385
+ raise if fail_on_error
386
+ end
387
+ end
388
+ end
389
+ rescue
390
+ stop
391
+ raise
392
+ end
393
+
394
+ def stop_servers(host_params)
395
+ if not host_params.nil?
396
+ host_params.each do |host_param|
397
+ host, port = host_param.split(':')
398
+ if @servers_running.has_key?(host_param)
399
+ begin
400
+ logger.info("Stopping messagebus consumer client for #{host_param}")
401
+ client = @servers_running[host_param]
402
+ stop_server(client)
403
+ @servers_running.delete(host_param)
404
+ rescue => e
405
+ logger.error("Failed to stop server #{host_param} with exception: #{e.message}, #{e.backtrace.join("|")}")
406
+ end
407
+ end
408
+ end
409
+ end
410
+ end
411
+
412
+ def receive_internal(non_blocking=false)
413
+ current_time = Time.new();
414
+ if current_time > @refresh_time + @options.conn_lifetime_sec
415
+ refresh_servers
416
+ end
417
+
418
+ received_message = @received_messages.pop(non_blocking)
419
+ if received_message[:stop_processing_sentinel]
420
+ return received_message[:msg]
421
+ end
422
+
423
+ @last_received = received_message
424
+ if !@last_received.nil?
425
+ original_message = @last_received[:msg]
426
+ if original_message.command == 'ERROR'
427
+ logger.error("received error frame from server:\n#{original_message}")
428
+ raise ErrorFrameReceived, "received error frame from server:\n#{original_message}"
429
+ else
430
+ message = @last_received[:decoded_msg]
431
+ logger.info("received message with id:#{message.message_id}")
432
+
433
+ # send back credits for this message.
434
+ credit
435
+
436
+ # send back the ack if user has choosen autoClient ack mode.
437
+ if @options.ack_type == Messagebus::ACK_TYPE_AUTO_CLIENT
438
+ ack
439
+ end
440
+
441
+ return message
442
+ end
443
+ end
444
+ end
445
+
446
+ end
447
+ end