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,583 @@
1
+ require 'socket'
2
+ require 'timeout'
3
+ require 'io/wait'
4
+ require 'stomp/errors'
5
+ require 'stomp/message'
6
+ require 'stomp/ext/hash'
7
+
8
+ module Stomp
9
+
10
+ # Low level connection which maps commands and supports
11
+ # synchronous receives
12
+ class Connection
13
+ attr_reader :connection_frame
14
+ attr_reader :disconnect_receipt
15
+ #alias :obj_send :send
16
+
17
+ def self.default_port(ssl)
18
+ ssl ? 61612 : 61613
19
+ end
20
+
21
+ # A new Connection object accepts the following parameters:
22
+ #
23
+ # login (String, default : '')
24
+ # passcode (String, default : '')
25
+ # host (String, default : 'localhost')
26
+ # port (Integer, default : 61613)
27
+ # reliable (Boolean, default : false)
28
+ # reconnect_delay (Integer, default : 5)
29
+ #
30
+ # e.g. c = Connection.new("username", "password", "localhost", 61613, true)
31
+ #
32
+ # Hash:
33
+ #
34
+ # hash = {
35
+ # :hosts => [
36
+ # {:login => "login1", :passcode => "passcode1", :host => "localhost", :port => 61616, :ssl => false},
37
+ # {:login => "login2", :passcode => "passcode2", :host => "remotehost", :port => 61617, :ssl => false}
38
+ # ],
39
+ # :initial_reconnect_delay => 0.01,
40
+ # :max_reconnect_delay => 30.0,
41
+ # :use_exponential_back_off => true,
42
+ # :back_off_multiplier => 2,
43
+ # :max_reconnect_attempts => 0,
44
+ # :randomize => false,
45
+ # :backup => false,
46
+ # :timeout => -1,
47
+ # :connect_headers => {},
48
+ # :parse_timeout => 5,
49
+ # :logger => nil,
50
+ # }
51
+ #
52
+ # e.g. c = Connection.new(hash)
53
+ #
54
+ # TODO
55
+ # Stomp URL :
56
+ # A Stomp URL must begin with 'stomp://' and can be in one of the following forms:
57
+ #
58
+ # stomp://host:port
59
+ # stomp://host.domain.tld:port
60
+ # stomp://user:pass@host:port
61
+ # stomp://user:pass@host.domain.tld:port
62
+ #
63
+ def initialize(login = '', passcode = '', host = 'localhost', port = 61613, reliable = false, reconnect_delay = 5, connect_headers = {})
64
+ @received_messages = []
65
+
66
+ if login.is_a?(Hash)
67
+ hashed_initialize(login)
68
+ else
69
+ @host = host
70
+ @port = port
71
+ @login = login
72
+ @passcode = passcode
73
+ @reliable = reliable
74
+ @reconnect_delay = reconnect_delay
75
+ @connect_headers = connect_headers
76
+ @ssl = false
77
+ @parse_timeout = 5 # To override, use hashed parameters
78
+ @logger = nil # To override, use hashed parameters
79
+ end
80
+
81
+ # Use Mutexes: only one lock per each thread
82
+ # Revert to original implementation attempt
83
+ @transmit_semaphore = Mutex.new
84
+ @read_semaphore = Mutex.new
85
+ @socket_semaphore = Mutex.new
86
+ @subscriptions = {}
87
+ @failure = nil
88
+ @connection_attempts = 0
89
+
90
+ socket
91
+ end
92
+
93
+ def hashed_initialize(params)
94
+ @parameters = refine_params(params)
95
+ @reliable = true
96
+ @reconnect_delay = @parameters[:initial_reconnect_delay]
97
+ @connect_headers = @parameters[:connect_headers]
98
+ @parse_timeout = @parameters[:parse_timeout]
99
+ @logger = Logger.new( @parameters[:logger].instance_variable_get( "@logdev" ).filename )
100
+ @logger.formatter = @parameters[:logger].formatter
101
+ #sets the first host to connect
102
+ change_host
103
+ if @logger && @logger.respond_to?(:on_connecting)
104
+ @logger.on_connecting(log_params)
105
+ end
106
+ end
107
+
108
+ def socket
109
+
110
+ @socket_semaphore.synchronize do
111
+ used_socket = @socket
112
+ used_socket = nil if closed?
113
+ while used_socket.nil? || !@failure.nil?
114
+ @failure = nil
115
+ begin
116
+
117
+ @logger.info("Opening socket : #{log_params}")
118
+ used_socket = open_socket
119
+ # Open complete
120
+
121
+ @logger.info("Opened socket : #{log_params}")
122
+ connect(used_socket)
123
+
124
+ @connection_attempts = 0
125
+ rescue
126
+ @failure = $!
127
+ used_socket = nil
128
+ raise unless @reliable
129
+
130
+ @logger.error "connect to #{@host} failed: #{$!} will retry(##{@connection_attempts}) in #{@reconnect_delay}\n"
131
+
132
+ if max_reconnect_attempts?
133
+ @reconnect_delay = 0.01
134
+ @connection_attempts = 0
135
+ raise Stomp::Error::MaxReconnectAttempts
136
+ end
137
+
138
+ sleep(@reconnect_delay)
139
+
140
+ @connection_attempts += 1
141
+
142
+ if @parameters
143
+ change_host
144
+ increase_reconnect_delay
145
+ end
146
+
147
+ unless @failure
148
+ @reconnect_delay = 0
149
+ end
150
+
151
+ end
152
+ end
153
+
154
+ @socket = used_socket
155
+ end
156
+ end
157
+
158
+ def refine_params(params)
159
+ params = params.uncamelize_and_symbolize_keys
160
+
161
+ default_params = {
162
+ :connect_headers => {},
163
+ # Failover parameters
164
+ :initial_reconnect_delay => 0.01,
165
+ :max_reconnect_delay => 30.0,
166
+ :use_exponential_back_off => true,
167
+ :back_off_multiplier => 2,
168
+ :max_reconnect_attempts => 2,
169
+ :randomize => false,
170
+ :backup => false,
171
+ :timeout => -1,
172
+ # Parse Timeout
173
+ :parse_timeout => 5
174
+ }
175
+
176
+ default_params.merge(params)
177
+
178
+ end
179
+
180
+ def change_host
181
+ @parameters[:hosts] = @parameters[:hosts].sort_by { rand } if @parameters[:randomize]
182
+
183
+ # Set first as master and send it to the end of array
184
+ current_host = @parameters[:hosts].shift
185
+ @parameters[:hosts] << current_host
186
+
187
+ @ssl = current_host[:ssl]
188
+ @host = current_host[:host]
189
+ @port = current_host[:port] || Connection::default_port(@ssl)
190
+ @login = current_host[:login] || ""
191
+ @passcode = current_host[:passcode] || ""
192
+
193
+ end
194
+
195
+ def max_reconnect_attempts?
196
+ !(@parameters.nil? || @parameters[:max_reconnect_attempts].nil?) && @parameters[:max_reconnect_attempts] != 0 && @connection_attempts >= @parameters[:max_reconnect_attempts]
197
+ end
198
+
199
+ def increase_reconnect_delay
200
+
201
+ @reconnect_delay *= @parameters[:back_off_multiplier] if @parameters[:use_exponential_back_off]
202
+ @reconnect_delay = @parameters[:max_reconnect_delay] if @reconnect_delay > @parameters[:max_reconnect_delay]
203
+ @reconnect_delay
204
+ end
205
+
206
+ # Is this connection open?
207
+ def open?
208
+ !@closed
209
+ end
210
+
211
+ # Is this connection closed?
212
+ def closed?
213
+ @closed
214
+ end
215
+
216
+ # Begin a transaction, requires a name for the transaction
217
+ def begin(name, headers = {})
218
+ headers[:transaction] = name
219
+ transmit("BEGIN", headers)
220
+ end
221
+
222
+ # Acknowledge a message, used when a subscription has specified
223
+ # client acknowledgement ( connection.subscribe "/queue/a", :ack => 'client'g
224
+ #
225
+ # Accepts a transaction header ( :transaction => 'some_transaction_id' )
226
+ def ack(message_id, headers = {})
227
+ headers['message-id'] = message_id
228
+ transmit("ACK", headers)
229
+ end
230
+
231
+ # Nacknowledge a message, used when a subscription has specified
232
+ # client acknowledgement ( connection.subscribe "/queue/a", :ack => 'client'g
233
+ #
234
+ # Accepts a transaction header ( :transaction => 'some_transaction_id' )
235
+ def nack(message_id, headers = {})
236
+ headers['message-id'] = message_id
237
+ transmit("NACK", headers)
238
+ end
239
+
240
+ # Sends a keepalive frame to the server so that stale connection won't be reclaimed.
241
+ #
242
+ # Accepts a transaction header ( :transaction => 'some_transaction_id' )
243
+ def keepalive(headers = {})
244
+ transmit("KEEPALIVE", headers)
245
+ end
246
+
247
+ # Credit a message, used when a subscription has specified
248
+ def credit(message_id, headers = {})
249
+ headers['message-id'] = message_id
250
+ transmit("CREDIT", headers)
251
+ end
252
+
253
+ # Commit a transaction by name
254
+ def commit(name, headers = {})
255
+ headers[:transaction] = name
256
+ transmit("COMMIT", headers)
257
+ end
258
+
259
+ # Abort a transaction by name
260
+ def abort(name, headers = {})
261
+ headers[:transaction] = name
262
+ transmit("ABORT", headers)
263
+ end
264
+
265
+ # Subscribe to a destination, must specify a name
266
+ def subscribe(name, headers = {}, subId = nil)
267
+ headers[:destination] = name
268
+ transmit("SUBSCRIBE", headers)
269
+
270
+ # Store the sub so that we can replay if we reconnect.
271
+ if @reliable
272
+ subId = name if subId.nil?
273
+ @subscriptions[subId] = headers
274
+ end
275
+ end
276
+
277
+ # Unsubscribe from a destination, must specify a name
278
+ def unsubscribe(name, headers = {}, subId = nil)
279
+ headers[:destination] = name
280
+ transmit("UNSUBSCRIBE", headers)
281
+ if @reliable
282
+ subId = name if subId.nil?
283
+ @subscriptions.delete(subId)
284
+ end
285
+ end
286
+
287
+ # Publish message to destination
288
+ #
289
+ # To disable content length header ( :suppress_content_length => true )
290
+ # Accepts a transaction header ( :transaction => 'some_transaction_id' )
291
+ def publish(destination, message, headers = {})
292
+ headers[:destination] = destination
293
+ transmit("SEND", headers, message)
294
+ end
295
+
296
+ def obj_send(*args)
297
+ __send__(*args)
298
+ end
299
+
300
+ # Send a message back to the source or to the dead letter queue
301
+ #
302
+ # Accepts a dead letter queue option ( :dead_letter_queue => "/queue/DLQ" )
303
+ # Accepts a limit number of redeliveries option ( :max_redeliveries => 6 )
304
+ # Accepts a force client acknowledgement option (:force_client_ack => true)
305
+ def unreceive(message, options = {})
306
+ options = { :dead_letter_queue => "/queue/DLQ", :max_redeliveries => 6 }.merge options
307
+ # Lets make sure all keys are symbols
308
+ message.headers = message.headers.symbolize_keys
309
+
310
+ retry_count = message.headers[:retry_count].to_i || 0
311
+ message.headers[:retry_count] = retry_count + 1
312
+ transaction_id = "transaction-#{message.headers[:'message-id']}-#{retry_count}"
313
+ message_id = message.headers.delete(:'message-id')
314
+
315
+ begin
316
+ self.begin transaction_id
317
+
318
+ if client_ack?(message) || options[:force_client_ack]
319
+ self.ack(message_id, :transaction => transaction_id)
320
+ end
321
+
322
+ if retry_count <= options[:max_redeliveries]
323
+ self.publish(message.headers[:destination], message.body, message.headers.merge(:transaction => transaction_id))
324
+ else
325
+ # Poison ack, sending the message to the DLQ
326
+ self.publish(options[:dead_letter_queue], message.body, message.headers.merge(:transaction => transaction_id, :original_destination => message.headers[:destination], :persistent => true))
327
+ end
328
+ self.commit transaction_id
329
+ rescue Exception => exception
330
+ self.abort transaction_id
331
+ raise exception
332
+ end
333
+ end
334
+
335
+ def client_ack?(message)
336
+ headers = @subscriptions[message.headers[:destination]]
337
+ !headers.nil? && headers[:ack] == "client"
338
+ end
339
+
340
+ # Close this connection
341
+ def disconnect(headers = {})
342
+ transmit("DISCONNECT", headers)
343
+ headers = headers.symbolize_keys
344
+ @disconnect_receipt = receive if headers[:receipt]
345
+ @logger.info("Connection disconnected for : #{log_params}")
346
+ close_socket
347
+ end
348
+
349
+ # Return a pending message if one is available, otherwise
350
+ # return nil
351
+ def poll
352
+ # No need for a read lock here. The receive method eventually fulfills
353
+ # that requirement.
354
+ return nil if @socket.nil? || !@socket.ready?
355
+ receive
356
+ end
357
+
358
+ # Receive a frame, repeatedly retries until the frame is received
359
+ def __old_receive
360
+ # The receive may fail so we may need to retry.
361
+ while TRUE
362
+ begin
363
+
364
+ used_socket = socket
365
+ return _receive(used_socket)
366
+ rescue => e
367
+ @failure = $!
368
+ raise unless @reliable
369
+ @logger.error "receive failed: #{e}\n #{e.backtrace.join("\n")}"
370
+ nil
371
+ end
372
+ end
373
+ end
374
+
375
+ def receive
376
+ super_result = __old_receive
377
+ if super_result.nil? && @reliable && !closed?
378
+ errstr = "connection.receive returning EOF as nil - clearing socket, resetting connection.\n"
379
+ @logger.info(errstr)
380
+ @socket = nil
381
+ super_result = __old_receive
382
+ end
383
+ return super_result
384
+ end
385
+
386
+ private
387
+
388
+ def _receive( read_socket )
389
+
390
+ while TRUE
391
+ begin
392
+ @read_semaphore.synchronize do
393
+ # Throw away leading newlines, which are actually trailing
394
+ # newlines from the preceding message.
395
+ Timeout::timeout(@parse_timeout, Stomp::Error::PacketReceiptTimeout) do
396
+ begin
397
+ last_char = read_socket.getc
398
+ return nil if last_char.nil?
399
+ end until parse_char(last_char) != "\n"
400
+ read_socket.ungetc(last_char)
401
+ end
402
+
403
+ # If the reading hangs for more than X seconds, abort the parsing process.
404
+ # X defaults to 5. Override allowed in connection hash parameters.
405
+ Timeout::timeout(@parse_timeout, Stomp::Error::PacketParsingTimeout) do
406
+
407
+ # Reads the beginning of the message until it runs into a empty line
408
+ message_header = ''
409
+ line = ''
410
+ begin
411
+ message_header << line
412
+ line = read_socket.gets
413
+ return nil if line.nil?
414
+ end until line =~ /^\s?\n$/
415
+
416
+ # Checks if it includes content_length header
417
+ content_length = message_header.match /content-length\s?:\s?(\d+)\s?\n/
418
+ message_body = ''
419
+
420
+ # If it does, reads the specified amount of bytes
421
+ char = ''
422
+ if content_length
423
+ message_body = read_socket.read content_length[1].to_i
424
+ raise Stomp::Error::InvalidMessageLength unless parse_char(read_socket.getc) == "\0"
425
+ # Else reads, the rest of the message until the first \0
426
+ else
427
+ message_body << char while (char = parse_char(read_socket.getc)) != "\0"
428
+ end
429
+
430
+ # Adds the excluded \n and \0 and tries to create a new message with it
431
+ return Message.new(message_header + "\n" + message_body + "\0")
432
+ end
433
+ end
434
+ rescue Stomp::Error::PacketReceiptTimeout =>pe
435
+ @logger.debug("Packet not received. Continuing")
436
+ sleep 0.01
437
+ next
438
+ rescue Exception => e
439
+ @logger.info(e.inspect)
440
+ end
441
+ break
442
+ end
443
+ end
444
+
445
+ def parse_char(char)
446
+ RUBY_VERSION > '1.9' ? char : char.chr
447
+ end
448
+
449
+ def transmit(command, headers = {}, body = '')
450
+
451
+ # The transmit may fail so we may need to retry.
452
+ # But we should have some decent limit on retries
453
+ max_transmit_attempts = 5
454
+ transmit_attempts = 0
455
+
456
+ while TRUE
457
+ begin
458
+ used_socket = socket
459
+ _transmit(used_socket, command, headers, body)
460
+ return
461
+ rescue Stomp::Error::MaxReconnectAttempts => e
462
+ errstr = "transmit to #{@host} failed after max connection retry attempts: #{$!}\n Stack Trace: #{caller}"
463
+ @logger.error(errstr)
464
+ @failure = $!
465
+ raise
466
+ rescue => e
467
+ @failure = $!
468
+ if transmit_attempts >= max_transmit_attempts
469
+ errstr = "transmit to #{@host} failed after max transmit retry attempts: #{$!}\n Stack Trace: #{caller}"
470
+ @logger.error(errstr)
471
+ raise Stomp::Error::MaxReconnectAttempts
472
+ end
473
+ transmit_attempts = transmit_attempts + 1
474
+ end
475
+ end
476
+ end
477
+
478
+ def _transmit(used_socket, command, headers = {}, body = '')
479
+ @transmit_semaphore.synchronize do
480
+ # Handle nil body
481
+ body = '' if body.nil?
482
+ # The content-length should be expressed in bytes.
483
+ # Ruby 1.8: String#length => # of bytes; Ruby 1.9: String#length => # of characters
484
+ # With Unicode strings, # of bytes != # of characters. So, use String#bytesize when available.
485
+ body_length_bytes = body.respond_to?(:bytesize) ? body.bytesize : body.length
486
+
487
+ # ActiveMQ interprets every message as a BinaryMessage
488
+ # if content_length header is included.
489
+ # Using :suppress_content_length => true will suppress this behaviour
490
+ # and ActiveMQ will interpret the message as a TextMessage.
491
+ # For more information refer to http://juretta.com/log/2009/05/24/activemq-jms-stomp/
492
+ # Lets send this header in the message, so it can maintain state when using unreceive
493
+ headers['content-length'] = "#{body_length_bytes}" unless headers[:suppress_content_length]
494
+ used_socket.puts command
495
+ headers.each {|k,v| used_socket.puts "#{k}:#{v}" }
496
+ used_socket.puts "content-type: text/plain; charset=UTF-8"
497
+ used_socket.puts
498
+ used_socket.write body
499
+ used_socket.write "\0"
500
+ end
501
+ end
502
+
503
+ def open_tcp_socket
504
+ tcp_socket = TCPSocket.open @host, @port
505
+
506
+ tcp_socket
507
+ end
508
+
509
+ def open_ssl_socket
510
+ require 'openssl' unless defined?(OpenSSL)
511
+ ctx = OpenSSL::SSL::SSLContext.new
512
+
513
+ # For client certificate authentication:
514
+ # key_path = ENV["STOMP_KEY_PATH"] || "~/stomp_keys"
515
+ # ctx.cert = OpenSSL::X509::Certificate.new("#{key_path}/client.cer")
516
+ # ctx.key = OpenSSL::PKey::RSA.new("#{key_path}/client.keystore")
517
+
518
+ # For server certificate authentication:
519
+ # truststores = OpenSSL::X509::Store.new
520
+ # truststores.add_file("#{key_path}/client.ts")
521
+ # ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER
522
+ # ctx.cert_store = truststores
523
+
524
+ ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
525
+
526
+ ssl = OpenSSL::SSL::SSLSocket.new(open_tcp_socket, ctx)
527
+ def ssl.ready?
528
+ ! @rbuffer.empty? || @io.ready?
529
+ end
530
+ ssl.connect
531
+ ssl
532
+ end
533
+
534
+ def close_socket
535
+ begin
536
+ # Need to set @closed = true before closing the socket
537
+ # within the @read_semaphore thread
538
+ @read_semaphore.synchronize do
539
+ @closed = true
540
+ @socket.close
541
+ end
542
+ rescue
543
+ #Ignoring if already closed
544
+ end
545
+ @closed
546
+ end
547
+
548
+ def open_socket
549
+ used_socket = @ssl ? open_ssl_socket : open_tcp_socket
550
+ # try to close the old connection if any
551
+ close_socket
552
+
553
+ @closed = false
554
+ # Use keepalive
555
+ used_socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
556
+ used_socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
557
+ used_socket
558
+ end
559
+
560
+ def connect(used_socket)
561
+ headers = @connect_headers.clone
562
+ headers[:login] = @login
563
+ headers[:passcode] = @passcode
564
+ _transmit(used_socket, "CONNECT", headers)
565
+
566
+ @connection_frame = _receive(used_socket)
567
+ @disconnect_receipt = nil
568
+ # replay any subscriptions.
569
+ @subscriptions.each { |k,v| _transmit(used_socket, "SUBSCRIBE", v) }
570
+ end
571
+
572
+ def log_params
573
+ #lparms = @parameters.clone
574
+ lparms = Hash.new
575
+ lparms[:cur_host] = @host
576
+ lparms[:cur_port] = @port
577
+ lparms[:cur_recondelay] = @reconnect_delay
578
+ lparms[:cur_conattempts] = @connection_attempts
579
+ lparms
580
+ end
581
+ end
582
+
583
+ end