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.
- checksums.yaml +7 -0
- data/README.mediawiki +169 -0
- data/Rakefile +24 -0
- data/bin/console +11 -0
- data/bin/messagebus_swarm +77 -0
- data/lib/messagebus.rb +62 -0
- data/lib/messagebus/client.rb +166 -0
- data/lib/messagebus/cluster_map.rb +161 -0
- data/lib/messagebus/connection.rb +118 -0
- data/lib/messagebus/consumer.rb +447 -0
- data/lib/messagebus/custom_errors.rb +37 -0
- data/lib/messagebus/dottable_hash.rb +113 -0
- data/lib/messagebus/error_status.rb +42 -0
- data/lib/messagebus/logger.rb +45 -0
- data/lib/messagebus/message.rb +168 -0
- data/lib/messagebus/messagebus_types.rb +107 -0
- data/lib/messagebus/producer.rb +187 -0
- data/lib/messagebus/swarm.rb +49 -0
- data/lib/messagebus/swarm/controller.rb +296 -0
- data/lib/messagebus/swarm/drone.rb +195 -0
- data/lib/messagebus/swarm/drone/logging_worker.rb +53 -0
- data/lib/messagebus/validations.rb +68 -0
- data/lib/messagebus/version.rb +36 -0
- data/messagebus.gemspec +29 -0
- data/spec/messagebus/client_spec.rb +157 -0
- data/spec/messagebus/cluster_map_spec.rb +178 -0
- data/spec/messagebus/consumer_spec.rb +338 -0
- data/spec/messagebus/dottable_hash_spec.rb +137 -0
- data/spec/messagebus/message_spec.rb +93 -0
- data/spec/messagebus/producer_spec.rb +147 -0
- data/spec/messagebus/swarm/controller_spec.rb +73 -0
- data/spec/messagebus/validations_spec.rb +71 -0
- data/spec/spec_helper.rb +10 -0
- data/vendor/gems/stomp.rb +23 -0
- data/vendor/gems/stomp/client.rb +360 -0
- data/vendor/gems/stomp/connection.rb +583 -0
- data/vendor/gems/stomp/errors.rb +39 -0
- data/vendor/gems/stomp/ext/hash.rb +24 -0
- data/vendor/gems/stomp/message.rb +68 -0
- 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
|