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