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,71 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'spec_helper')
|
2
|
+
|
3
|
+
describe Messagebus::Validations do
|
4
|
+
describe "#validate_destination_config" do
|
5
|
+
before do
|
6
|
+
@object = Object.new
|
7
|
+
@object.extend Messagebus::Validations
|
8
|
+
end
|
9
|
+
|
10
|
+
it "requires a destination name" do
|
11
|
+
lambda {
|
12
|
+
@object.validate_destination_config(nil)
|
13
|
+
}.should raise_error(Messagebus::InvalidDestination)
|
14
|
+
end
|
15
|
+
|
16
|
+
it "requires a topic to include a subscription_id" do
|
17
|
+
lambda {
|
18
|
+
@object.validate_destination_config("jms.topic.testTopic1", true)
|
19
|
+
}.should raise_error(Messagebus::InvalidDestination)
|
20
|
+
end
|
21
|
+
|
22
|
+
it "validates when a topic includes a subscription_id" do
|
23
|
+
lambda {
|
24
|
+
@object.validate_destination_config("jms.topic.testTopic1", true, :subscription_id => "test1")
|
25
|
+
}.should_not raise_error(Messagebus::InvalidDestination)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "#validate_connection_config" do
|
30
|
+
before do
|
31
|
+
@object = Object.new
|
32
|
+
@object.extend Messagebus::Validations
|
33
|
+
end
|
34
|
+
|
35
|
+
it "does not recognize an acknowledgement type other than AUTO_CLIENT AND CLIENT" do
|
36
|
+
lambda {
|
37
|
+
@object.validate_connection_config("localhost:61613", :ack_type => "")
|
38
|
+
}.should raise_error(Messagebus::InvalidAcknowledgementType)
|
39
|
+
end
|
40
|
+
|
41
|
+
it "requires host parameters" do
|
42
|
+
lambda {
|
43
|
+
@object.validate_connection_config(nil)
|
44
|
+
}.should raise_error(Messagebus::InvalidHost)
|
45
|
+
end
|
46
|
+
|
47
|
+
it "requires an array of host parameters" do
|
48
|
+
lambda {
|
49
|
+
@object.validate_connection_config("localhost:61613")
|
50
|
+
}.should raise_error(Messagebus::InvalidHost)
|
51
|
+
end
|
52
|
+
|
53
|
+
it "fails if the host is missing from the host parameters" do
|
54
|
+
lambda {
|
55
|
+
@object.validate_connection_config([":61613"])
|
56
|
+
}.should raise_error(Messagebus::InvalidHost)
|
57
|
+
end
|
58
|
+
|
59
|
+
it "fails if the port is missing from the host parameters" do
|
60
|
+
lambda {
|
61
|
+
@object.validate_connection_config(["localhost"])
|
62
|
+
}.should raise_error(Messagebus::InvalidHost)
|
63
|
+
end
|
64
|
+
|
65
|
+
it "should not fail if host contains a zero" do
|
66
|
+
lambda {
|
67
|
+
@object.validate_connection_config(["somehost-001:61613"])
|
68
|
+
}.should_not raise_error(Messagebus::InvalidHost)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
require "rspec"
|
2
|
+
|
3
|
+
$:.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
|
4
|
+
$:.unshift(File.join(File.dirname(__FILE__), "..", "vendor/gems"))
|
5
|
+
|
6
|
+
require "stomp"
|
7
|
+
require 'messagebus'
|
8
|
+
require 'messagebus/swarm/controller'
|
9
|
+
require 'thrift'
|
10
|
+
require 'json'
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# Copyright 2005-2006 Brian McCallister
|
2
|
+
# Copyright 2006 LogicBlaze Inc.
|
3
|
+
#
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
5
|
+
# you may not use this file except in compliance with the License.
|
6
|
+
# You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13
|
+
# See the License for the specific language governing permissions and
|
14
|
+
# limitations under the License.
|
15
|
+
|
16
|
+
require 'stomp/ext/hash'
|
17
|
+
require 'stomp/connection'
|
18
|
+
require 'stomp/client'
|
19
|
+
require 'stomp/message'
|
20
|
+
require 'stomp/errors'
|
21
|
+
|
22
|
+
module Stomp
|
23
|
+
end
|
@@ -0,0 +1,360 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'digest/sha1'
|
3
|
+
require 'stomp/connection'
|
4
|
+
require 'logger'
|
5
|
+
|
6
|
+
module Stomp
|
7
|
+
|
8
|
+
# Typical Stomp client class. Uses a listener thread to receive frames
|
9
|
+
# from the server, any thread can send.
|
10
|
+
#
|
11
|
+
# Receives all happen in one thread, so consider not doing much processing
|
12
|
+
# in that thread if you have much message volume.
|
13
|
+
class Client
|
14
|
+
|
15
|
+
attr_reader :login, :passcode, :host, :port, :reliable, :parameters
|
16
|
+
|
17
|
+
DESTINATION_REGEX = Regexp.new(/jms\.(queue|topic)\.([a-zA-Z1-9\.]*)/)
|
18
|
+
|
19
|
+
#alias :obj_send :send
|
20
|
+
|
21
|
+
# A new Client object can be initialized using two forms:
|
22
|
+
#
|
23
|
+
# Standard positional parameters:
|
24
|
+
# login (String, default : '')
|
25
|
+
# passcode (String, default : '')
|
26
|
+
# host (String, default : 'localhost')
|
27
|
+
# port (Integer, default : 61613)
|
28
|
+
# connect_headers (Map, default: {})
|
29
|
+
# reliable (Boolean, default : false)
|
30
|
+
# reconnect_delay (Integer, default : 5 ms)
|
31
|
+
def initialize(login = '', passcode = '', host = 'localhost', port = 61613, logger = '', connect_headers = {}, reliable = true, reconnect_delay = 5)
|
32
|
+
@login = login
|
33
|
+
@passcode = passcode
|
34
|
+
@host = host
|
35
|
+
@port = port.to_i
|
36
|
+
@reliable = reliable
|
37
|
+
@reconnect_delay = reconnect_delay
|
38
|
+
@connect_headers = connect_headers
|
39
|
+
|
40
|
+
check_arguments!
|
41
|
+
|
42
|
+
@id_mutex = Mutex.new
|
43
|
+
@ids = 1
|
44
|
+
|
45
|
+
# prepare connection parameters
|
46
|
+
params = Hash.new
|
47
|
+
hosts = [
|
48
|
+
{:login => login, :passcode => passcode, :host => host, :port => port, :ssl => false},
|
49
|
+
]
|
50
|
+
params['hosts'] = hosts
|
51
|
+
params['reliable'] = reliable
|
52
|
+
params['reconnect_delay'] = reconnect_delay
|
53
|
+
params['connect_headers'] = connect_headers
|
54
|
+
params['max_reconnect_attempts'] = 5
|
55
|
+
if logger == ''
|
56
|
+
@logger = Logger.new("/tmp/messagebus-client.log")
|
57
|
+
else
|
58
|
+
@logger = Logger.new( logger.instance_variable_get( "@logdev" ).filename )
|
59
|
+
@logger.formatter = logger.formatter
|
60
|
+
end
|
61
|
+
params['logger'] = @logger
|
62
|
+
|
63
|
+
@connection = Stomp::Connection.new(params)
|
64
|
+
|
65
|
+
start_listeners
|
66
|
+
end
|
67
|
+
|
68
|
+
# Syntactic sugar for 'Client.new' See 'initialize' for usage.
|
69
|
+
def self.open(login = '', passcode = '', host = 'localhost', port = 61613, reliable = false)
|
70
|
+
Client.new(login, passcode, host, port, reliable)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Join the listener thread for this client,
|
74
|
+
# generally used to wait for a quit signal
|
75
|
+
def join(limit = nil)
|
76
|
+
@listener_thread.join(limit)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Begin a transaction by name
|
80
|
+
def begin(name, headers = {})
|
81
|
+
@connection.begin(name, headers)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Abort a transaction by name
|
85
|
+
def abort(name, headers = {})
|
86
|
+
@connection.abort(name, headers)
|
87
|
+
|
88
|
+
# lets replay any ack'd messages in this transaction
|
89
|
+
replay_list = @replay_messages_by_txn[name]
|
90
|
+
if replay_list
|
91
|
+
replay_list.each do |message|
|
92
|
+
if listener = find_listener(message)
|
93
|
+
listener.call(message)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Commit a transaction by name
|
100
|
+
def commit(name, headers = {})
|
101
|
+
txn_id = headers[:transaction]
|
102
|
+
@replay_messages_by_txn.delete(txn_id)
|
103
|
+
@connection.commit(name, headers)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Subscribe to a destination, must be passed a block
|
107
|
+
# which will be used as a callback listener
|
108
|
+
#
|
109
|
+
# Accepts a transaction header ( :transaction => 'some_transaction_id' )
|
110
|
+
def subscribe(destination, headers = {})
|
111
|
+
raise "No listener given" unless block_given?
|
112
|
+
# use subscription id to correlate messages to subscription. As described in
|
113
|
+
# the SUBSCRIPTION section of the protocol: http://stomp.codehaus.org/Protocol.
|
114
|
+
# If no subscription id is provided, generate one.
|
115
|
+
set_subscription_id_if_missing(destination, headers)
|
116
|
+
if @listeners[headers[:id]]
|
117
|
+
raise "attempting to subscribe to a queue with a previous subscription"
|
118
|
+
end
|
119
|
+
@listeners[headers[:id]] = lambda {|msg| yield msg}
|
120
|
+
|
121
|
+
# bbansal: Groupon: Also add an error handler.
|
122
|
+
if not @listeners.include?("error_consumer")
|
123
|
+
@listeners["error_consumer"] = lambda {|msg| yield msg}
|
124
|
+
end
|
125
|
+
|
126
|
+
@connection.subscribe(destination, headers)
|
127
|
+
end
|
128
|
+
|
129
|
+
# Unsubecribe from a channel
|
130
|
+
def unsubscribe(name, headers = {})
|
131
|
+
set_subscription_id_if_missing(name, headers)
|
132
|
+
@connection.unsubscribe(name, headers)
|
133
|
+
@listeners[headers[:id]] = nil
|
134
|
+
end
|
135
|
+
|
136
|
+
# Acknowledge a message, used when a subscription has specified
|
137
|
+
# client acknowledgement ( connection.subscribe "/queue/a", :ack => 'client'g
|
138
|
+
#
|
139
|
+
# Accepts a transaction header ( :transaction => 'some_transaction_id' )
|
140
|
+
def acknowledge(message, headers = {})
|
141
|
+
txn_id = headers[:transaction]
|
142
|
+
if txn_id
|
143
|
+
# lets keep around messages ack'd in this transaction in case we rollback
|
144
|
+
replay_list = @replay_messages_by_txn[txn_id]
|
145
|
+
if replay_list.nil?
|
146
|
+
replay_list = []
|
147
|
+
@replay_messages_by_txn[txn_id] = replay_list
|
148
|
+
end
|
149
|
+
replay_list << message
|
150
|
+
end
|
151
|
+
if block_given?
|
152
|
+
headers['receipt'] = register_receipt_listener lambda {|r| yield r}
|
153
|
+
end
|
154
|
+
@connection.ack message.headers['message-id'], headers
|
155
|
+
end
|
156
|
+
|
157
|
+
# Nack a message, used when a subscription has specified
|
158
|
+
# client Nacknowledgement ( connection.subscribe "/queue/a", :ack => 'client'g
|
159
|
+
#
|
160
|
+
# Accepts a transaction header ( :transaction => 'some_transaction_id' )
|
161
|
+
def nack(message, headers = {})
|
162
|
+
if block_given?
|
163
|
+
headers['receipt'] = register_receipt_listener lambda {|r| yield r}
|
164
|
+
end
|
165
|
+
@connection.nack message.headers['message-id'], headers
|
166
|
+
end
|
167
|
+
|
168
|
+
def keepalive( headers = {} )
|
169
|
+
if block_given?
|
170
|
+
headers['receipt'] = register_receipt_listener lambda {|r| yield r}
|
171
|
+
end
|
172
|
+
@connection.keepalive(headers)
|
173
|
+
end
|
174
|
+
|
175
|
+
# Sends credit back to the server.
|
176
|
+
def credit(message, headers = {})
|
177
|
+
@connection.credit message.headers['message-id'], headers
|
178
|
+
end
|
179
|
+
|
180
|
+
# Unreceive a message, sending it back to its queue or to the DLQ
|
181
|
+
#
|
182
|
+
def unreceive(message, options = {})
|
183
|
+
@connection.unreceive(message, options)
|
184
|
+
end
|
185
|
+
|
186
|
+
# Publishes message to destination
|
187
|
+
#
|
188
|
+
# If a block is given a receipt will be requested and passed to the
|
189
|
+
# block on receipt
|
190
|
+
#
|
191
|
+
# Accepts a transaction header ( :transaction => 'some_transaction_id' )
|
192
|
+
def publish(destination, message, headers = {})
|
193
|
+
if block_given?
|
194
|
+
headers['receipt'] = register_receipt_listener lambda {|r| yield r}
|
195
|
+
end
|
196
|
+
@connection.publish(destination, message, headers)
|
197
|
+
end
|
198
|
+
|
199
|
+
def obj_send(*args)
|
200
|
+
__send__(*args)
|
201
|
+
end
|
202
|
+
|
203
|
+
def send(*args)
|
204
|
+
warn("This method is deprecated and will be removed on the next release. Use 'publish' instead")
|
205
|
+
publish(*args)
|
206
|
+
end
|
207
|
+
|
208
|
+
def connection_frame
|
209
|
+
@connection.connection_frame
|
210
|
+
end
|
211
|
+
|
212
|
+
def disconnect_receipt
|
213
|
+
@connection.disconnect_receipt
|
214
|
+
end
|
215
|
+
|
216
|
+
# Is this client open?
|
217
|
+
def open?
|
218
|
+
@connection.open?
|
219
|
+
end
|
220
|
+
|
221
|
+
# Is this client closed?
|
222
|
+
def closed?
|
223
|
+
@connection.closed?
|
224
|
+
end
|
225
|
+
|
226
|
+
# Close out resources in use by this client
|
227
|
+
def close headers={}
|
228
|
+
@listener_thread.exit
|
229
|
+
@connection.disconnect headers
|
230
|
+
end
|
231
|
+
|
232
|
+
# Check if the thread was created and isn't dead
|
233
|
+
def running
|
234
|
+
@listener_thread && !!@listener_thread.status
|
235
|
+
end
|
236
|
+
|
237
|
+
def get_connection
|
238
|
+
@connection
|
239
|
+
end
|
240
|
+
|
241
|
+
private
|
242
|
+
# Set a subscription id in the headers hash if one does not already exist.
|
243
|
+
# For simplicities sake, all subscriptions have a subscription ID.
|
244
|
+
# setting an id in the SUBSCRIPTION header is described in the stomp protocol docs:
|
245
|
+
# http://stomp.codehaus.org/Protocol
|
246
|
+
def set_subscription_id_if_missing(destination, headers)
|
247
|
+
headers[:id] = headers[:id] ? headers[:id] : headers['id']
|
248
|
+
if headers[:id] == nil
|
249
|
+
headers[:id] = Digest::SHA1.hexdigest(destination)
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
def register_receipt_listener(listener)
|
254
|
+
id = -1
|
255
|
+
@id_mutex.synchronize do
|
256
|
+
id = @ids.to_s
|
257
|
+
@ids = @ids.succ
|
258
|
+
end
|
259
|
+
@receipt_listeners[id] = listener
|
260
|
+
return id
|
261
|
+
end
|
262
|
+
|
263
|
+
# e.g. login:passcode@host:port or host:port
|
264
|
+
def url_regex
|
265
|
+
'(([\w\.\-]*):(\w*)@)?([\w\.\-]+):(\d+)'
|
266
|
+
end
|
267
|
+
|
268
|
+
def parse_hosts(url)
|
269
|
+
hosts = []
|
270
|
+
|
271
|
+
host_match = /stomp(\+ssl)?:\/\/(([\w\.]*):(\w*)@)?([\w\.]+):(\d+)\)/
|
272
|
+
url.scan(host_match).each do |match|
|
273
|
+
host = {}
|
274
|
+
host[:ssl] = !match[0].nil?
|
275
|
+
host[:login] = match[2] || ""
|
276
|
+
host[:passcode] = match[3] || ""
|
277
|
+
host[:host] = match[4]
|
278
|
+
host[:port] = match[5].to_i
|
279
|
+
|
280
|
+
hosts << host
|
281
|
+
end
|
282
|
+
|
283
|
+
hosts
|
284
|
+
end
|
285
|
+
|
286
|
+
def check_arguments!
|
287
|
+
raise ArgumentError if @host.nil? || @host.empty?
|
288
|
+
raise ArgumentError if @port.nil? || @port == '' || @port < 1 || @port > 65535
|
289
|
+
raise ArgumentError unless @reliable.is_a?(TrueClass) || @reliable.is_a?(FalseClass)
|
290
|
+
end
|
291
|
+
|
292
|
+
def filter_options(options)
|
293
|
+
new_options = {}
|
294
|
+
new_options[:initial_reconnect_delay] = (options["initialReconnectDelay"] || 10).to_f / 1000 # In ms
|
295
|
+
new_options[:max_reconnect_delay] = (options["maxReconnectDelay"] || 30000 ).to_f / 1000 # In ms
|
296
|
+
new_options[:use_exponential_back_off] = !(options["useExponentialBackOff"] == "false") # Default: true
|
297
|
+
new_options[:back_off_multiplier] = (options["backOffMultiplier"] || 2 ).to_i
|
298
|
+
new_options[:max_reconnect_attempts] = (options["maxReconnectAttempts"] || 0 ).to_i
|
299
|
+
new_options[:randomize] = options["randomize"] == "true" # Default: false
|
300
|
+
new_options[:backup] = false # Not implemented yet: I'm using a master X slave solution
|
301
|
+
new_options[:timeout] = -1 # Not implemented yet: a "timeout(5) do ... end" would do the trick, feel free
|
302
|
+
|
303
|
+
new_options
|
304
|
+
end
|
305
|
+
|
306
|
+
def find_listener(message)
|
307
|
+
subscription_id = message.headers['subscription']
|
308
|
+
if subscription_id == nil
|
309
|
+
# For backward compatibility, some messages may already exist with no
|
310
|
+
# subscription id, in which case we can attempt to synthesize one.
|
311
|
+
set_subscription_id_if_missing(message.headers['destination'], message.headers)
|
312
|
+
subscription_id = message.headers['id']
|
313
|
+
end
|
314
|
+
@listeners[subscription_id]
|
315
|
+
end
|
316
|
+
|
317
|
+
def start_listeners
|
318
|
+
@listeners = {}
|
319
|
+
@receipt_listeners = {}
|
320
|
+
@replay_messages_by_txn = {}
|
321
|
+
|
322
|
+
@listener_thread = Thread.start do
|
323
|
+
begin
|
324
|
+
while true
|
325
|
+
begin
|
326
|
+
message = @connection.receive
|
327
|
+
@logger.info "Listener receives #{message.inspect}"
|
328
|
+
|
329
|
+
if message.command == 'MESSAGE'
|
330
|
+
if listener = @listeners[message.headers['destination']]
|
331
|
+
listener.call(message)
|
332
|
+
elsif listener = @listeners[message.headers['subscription']]
|
333
|
+
# Also check for message.headers (subscription) for topics etc.
|
334
|
+
listener.call(message)
|
335
|
+
end
|
336
|
+
elsif message.command == 'RECEIPT'
|
337
|
+
if listener = @receipt_listeners[message.headers['receipt-id']]
|
338
|
+
listener.call(message)
|
339
|
+
end
|
340
|
+
elsif message.command == 'ERROR'
|
341
|
+
if listener = @receipt_listeners[message.headers['receipt-id']]
|
342
|
+
listener.call(message)
|
343
|
+
elsif listener = @listeners["error_consumer"]
|
344
|
+
# check if the error message have a destination string.
|
345
|
+
listener.call(message)
|
346
|
+
end
|
347
|
+
end
|
348
|
+
rescue Exception => e
|
349
|
+
@logger.error "Stomp listener failed. Will retry. #{e}\n" + e.backtrace.join("\n")
|
350
|
+
|
351
|
+
end
|
352
|
+
end
|
353
|
+
ensure
|
354
|
+
@logger.info "Listener thread is completed."
|
355
|
+
end
|
356
|
+
end
|
357
|
+
end
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|