stomp 1.1 → 1.1.3

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,3 +1,9 @@
1
+ == 1.1.3 2009-24-11
2
+
3
+ * Failover support
4
+ * SSL support
5
+ * Stomp::Connection and Stomp::Client accept a hash on their constructor
6
+
1
7
  == 1.1 2009-27-02
2
8
 
3
9
  * Ruby 1.9 Support
data/README.rdoc CHANGED
@@ -15,6 +15,45 @@ An implementation of the Stomp protocol (http://stomp.codehaus.org/Protocol) for
15
15
  p msg
16
16
  end
17
17
 
18
+ ===Failover + SSL Example URL Usage
19
+
20
+ options = "initialReconnectDelay=5000&randomize=false&useExponentialBackOff=false"
21
+
22
+ #remotehost1 uses SSL, remotehost2 doesn't
23
+ client = Stomp::Client.new("failover:(stomp+ssl://login1:passcode1@remotehost1:61612,stomp://login2:passcode2@remotehost2:61613)?#{options}")
24
+
25
+ client.send("/my/queue", "hello world!")
26
+ client.subscribe("/my/queue") do |msg|
27
+ p msg
28
+ end
29
+
30
+ ===Hash Example Usage
31
+
32
+ hash = {
33
+ :hosts => [
34
+ {:login => "login1", :passcode => "passcode1", :host => "remotehost1", :port => 61612, :ssl => true},
35
+ {:login => "login2", :passcode => "passcode2", :host => "remotehost2", :port => 61613, :ssl => false},
36
+
37
+ ],
38
+ # These are the default parameters, don't need to be set
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
+ }
49
+
50
+ # for client
51
+ client = Stomp::Client.new(hash)
52
+
53
+ # for connection
54
+ connection = Stomp::Connection.new(hash)
55
+
56
+
18
57
  ===Contact info
19
58
 
20
59
  Up until March 2009 the project was maintained and primarily developed by Brian McCallister.
data/lib/stomp/client.rb CHANGED
@@ -7,7 +7,8 @@ module Stomp
7
7
  # in that thread if you have much message volume.
8
8
  class Client
9
9
 
10
- attr_reader :login, :passcode, :host, :port, :reliable, :running
10
+ attr_reader :login, :passcode, :host, :port, :reliable, :running, :parameters
11
+ alias :obj_send :send
11
12
 
12
13
  # A new Client object can be initialized using two forms:
13
14
  #
@@ -30,22 +31,46 @@ module Stomp
30
31
  #
31
32
  def initialize(login = '', passcode = '', host = 'localhost', port = 61613, reliable = false)
32
33
 
33
- # Parse stomp:// URL's or set positional params
34
- case login
35
- when /stomp:\/\/([\w\.]+):(\d+)/ # e.g. stomp://host:port
36
- # grabs the matching positions out of the regex which are stored as
37
- # $1 (host), $2 (port), etc
38
- @login = ''
39
- @passcode = ''
40
- @host = $1
41
- @port = $2.to_i
42
- @reliable = false
43
- when /stomp:\/\/([\w\.]+):(\w+)@([\w\.]+):(\d+)/ # e.g. stomp://login:passcode@host:port
44
- @login = $1
45
- @passcode = $2
46
- @host = $3
47
- @port = $4.to_i
34
+ # Parse stomp:// URL's or set params
35
+ if login.is_a?(Hash)
36
+ @parameters = login
37
+
38
+ first_host = @parameters[:hosts][0]
39
+
40
+ @login = first_host[:login]
41
+ @passcode = first_host[:passcode]
42
+ @host = first_host[:host]
43
+ @port = first_host[:port] || default_port(first_host[:ssl])
44
+
45
+ @reliable = true
46
+
47
+ elsif login =~ /^stomp:\/\/(([\w\.]+):(\w+)@)?([\w\.]+):(\d+)/ # e.g. stomp://login:passcode@host:port or stomp://host:port
48
+ @login = $2 || ""
49
+ @passcode = $3 || ""
50
+ @host = $4
51
+ @port = $5.to_i
48
52
  @reliable = false
53
+ elsif login =~ /^failover:(\/\/)?\(stomp(\+ssl)?:\/\/(([\w\.]*):(\w*)@)?([\w\.]+):(\d+)(,stomp(\+ssl)?:\/\/(([\w\.]*):(\w*)@)?([\w\.]+):(\d+)\))+(\?(.*))?$/ # e.g. failover://(stomp://login1:passcode1@localhost:61616,stomp://login2:passcode2@remotehost:61617)?option1=param
54
+
55
+ first_host = {}
56
+ first_host[:ssl] = !$2.nil?
57
+ @login = first_host[:login] = $4 || ""
58
+ @passcode = first_host[:passcode] = $5 || ""
59
+ @host = first_host[:host] = $6
60
+ @port = first_host[:port] = $7.to_i || default_port(first_host[:ssl])
61
+
62
+ options = $16 || ""
63
+ parts = options.split(/&|=/)
64
+ options = Hash[*parts]
65
+
66
+ hosts = [first_host] + parse_hosts(login)
67
+
68
+ @parameters = {}
69
+ @parameters[:hosts] = hosts
70
+
71
+ @parameters.merge! filter_options(options)
72
+
73
+ @reliable = true
49
74
  else
50
75
  @login = login
51
76
  @passcode = passcode
@@ -54,38 +79,21 @@ module Stomp
54
79
  @reliable = reliable
55
80
  end
56
81
 
57
- raise ArgumentError if @host.nil? || @host.empty?
58
- raise ArgumentError if @port.nil? || @port == '' || @port < 1 || @port > 65535
59
- raise ArgumentError unless @reliable.is_a?(TrueClass) || @reliable.is_a?(FalseClass)
82
+ check_arguments!
60
83
 
61
84
  @id_mutex = Mutex.new
62
85
  @ids = 1
63
- @connection = Connection.new(@login, @passcode, @host, @port, @reliable)
64
- @listeners = {}
65
- @receipt_listeners = {}
66
- @running = true
67
- @replay_messages_by_txn = {}
68
-
69
- @listener_thread = Thread.start do
70
- while @running
71
- message = @connection.receive
72
- case
73
- when message.nil?
74
- break
75
- when message.command == 'MESSAGE'
76
- if listener = @listeners[message.headers['destination']]
77
- listener.call(message)
78
- end
79
- when message.command == 'RECEIPT'
80
- if listener = @receipt_listeners[message.headers['receipt-id']]
81
- listener.call(message)
82
- end
83
- end
84
- end
86
+
87
+ if @parameters
88
+ @connection = Connection.new(@parameters)
89
+ else
90
+ @connection = Connection.new(@login, @passcode, @host, @port, @reliable)
85
91
  end
92
+
93
+ start_listeners
86
94
 
87
95
  end
88
-
96
+
89
97
  # Syntactic sugar for 'Client.new' See 'initialize' for usage.
90
98
  def self.open(login = '', passcode = '', host = 'localhost', port = 61613, reliable = false)
91
99
  Client.new(login, passcode, host, port, reliable)
@@ -201,7 +209,76 @@ module Stomp
201
209
  @receipt_listeners[id] = listener
202
210
  id
203
211
  end
212
+
213
+ def default_port(ssl)
214
+ return 61612 if ssl
215
+
216
+ 61613
217
+ end
218
+
219
+ def parse_hosts(url)
220
+ hosts = []
221
+
222
+ host_match = /stomp(\+ssl)?:\/\/(([\w\.]*):(\w*)@)?([\w\.]+):(\d+)\)/
223
+ url.scan(host_match).each do |match|
224
+ host = {}
225
+ host[:ssl] = !match[0].nil?
226
+ host[:login] = match[2] || ""
227
+ host[:passcode] = match[3] || ""
228
+ host[:host] = match[4]
229
+ host[:port] = match[5].to_i
230
+
231
+ hosts << host
232
+ end
233
+
234
+ hosts
235
+ end
236
+
237
+ def check_arguments!
238
+ raise ArgumentError if @host.nil? || @host.empty?
239
+ raise ArgumentError if @port.nil? || @port == '' || @port < 1 || @port > 65535
240
+ raise ArgumentError unless @reliable.is_a?(TrueClass) || @reliable.is_a?(FalseClass)
241
+ end
242
+
243
+ def filter_options(options)
244
+ new_options = {}
245
+ new_options[:initial_reconnect_delay] = (options["initialReconnectDelay"] || 10).to_f / 1000 # In ms
246
+ new_options[:max_reconnect_delay] = (options["maxReconnectDelay"] || 30000 ).to_f / 1000 # In ms
247
+ new_options[:use_exponential_back_off] = !(options["useExponentialBackOff"] == "false") # Default: true
248
+ new_options[:back_off_multiplier] = (options["backOffMultiplier"] || 2 ).to_i
249
+ new_options[:max_reconnect_attempts] = (options["maxReconnectAttempts"] || 0 ).to_i
250
+ new_options[:randomize] = options["randomize"] == "true" # Default: false
251
+ new_options[:backup] = false # Not implemented yet: I'm using a master X slave solution
252
+ new_options[:timeout] = -1 # Not implemented yet: a "timeout(5) do ... end" would do the trick, feel free
253
+
254
+ new_options
255
+ end
256
+
257
+ def start_listeners
258
+ @listeners = {}
259
+ @receipt_listeners = {}
260
+ @running = true
261
+ @replay_messages_by_txn = {}
204
262
 
263
+ @listener_thread = Thread.start do
264
+ while @running
265
+ message = @connection.receive
266
+ case
267
+ when message.nil?
268
+ break
269
+ when message.command == 'MESSAGE'
270
+ if listener = @listeners[message.headers['destination']]
271
+ listener.call(message)
272
+ end
273
+ when message.command == 'RECEIPT'
274
+ if listener = @receipt_listeners[message.headers['receipt-id']]
275
+ listener.call(message)
276
+ end
277
+ end
278
+ end
279
+ end
280
+
281
+ end
205
282
  end
206
283
  end
207
284
 
@@ -4,7 +4,7 @@ module Stomp
4
4
  # synchronous receives
5
5
  class Connection
6
6
 
7
-
7
+ alias :obj_send :send
8
8
  # A new Connection object accepts the following parameters:
9
9
  #
10
10
  # login (String, default : '')
@@ -14,9 +14,28 @@ module Stomp
14
14
  # reliable (Boolean, default : false)
15
15
  # reconnect_delay (Integer, default : 5)
16
16
  #
17
- # e.g. c = Client.new("username", "password", "localhost", 61613, true)
17
+ # e.g. c = Connection.new("username", "password", "localhost", 61613, true)
18
+ #
19
+ # Hash:
20
+ #
21
+ # hash = {
22
+ # :hosts => [
23
+ # {:login => "login1", :passcode => "passcode1", :host => "localhost", :port => 61616, :ssl => false},
24
+ # {:login => "login2", :passcode => "passcode2", :host => "remotehost", :port => 61617, :ssl => false}
25
+ # ],
26
+ # :initial_reconnect_delay => 0.01,
27
+ # :max_reconnect_delay => 30.0,
28
+ # :use_exponential_back_off => true,
29
+ # :back_off_multiplier => 2,
30
+ # :max_reconnect_attempts => 0,
31
+ # :randomize => false,
32
+ # :backup => false,
33
+ # :timeout => -1
34
+ # }
35
+ #
36
+ # e.g. c = Connection.new(hash)
18
37
  #
19
- # TODO
38
+ # TODO
20
39
  # Stomp URL :
21
40
  # A Stomp URL must begin with 'stomp://' and can be in one of the following forms:
22
41
  #
@@ -26,22 +45,42 @@ module Stomp
26
45
  # stomp://user:pass@host.domain.tld:port
27
46
  #
28
47
  def initialize(login = '', passcode = '', host = 'localhost', port = 61613, reliable = false, reconnect_delay = 5, connect_headers = {})
29
- @host = host
30
- @port = port
31
- @login = login
32
- @passcode = passcode
48
+ if login.is_a?(Hash)
49
+ hashed_initialize(login)
50
+ else
51
+ @host = host
52
+ @port = port
53
+ @login = login
54
+ @passcode = passcode
55
+ @reliable = reliable
56
+ @reconnect_delay = reconnect_delay
57
+ @connect_headers = connect_headers
58
+ @ssl = false
59
+ @parameters = nil
60
+ end
61
+
33
62
  @transmit_semaphore = Mutex.new
34
63
  @read_semaphore = Mutex.new
35
64
  @socket_semaphore = Mutex.new
36
- @reliable = reliable
37
- @reconnect_delay = reconnect_delay
38
- @connect_headers = connect_headers
39
- @closed = false
65
+
40
66
  @subscriptions = {}
41
67
  @failure = nil
68
+ @connection_attempts = 0
69
+
42
70
  socket
43
71
  end
44
-
72
+
73
+ def hashed_initialize(params)
74
+
75
+ @parameters = refine_params(params)
76
+ @reliable = true
77
+ @reconnect_delay = @parameters[:initial_reconnect_delay]
78
+ @connect_headers = @parameters[:connect_headers]
79
+
80
+ #sets the first host to connect
81
+ change_host
82
+ end
83
+
45
84
  # Syntactic sugar for 'Connection.new' See 'initialize' for usage.
46
85
  def Connection.open(login = '', passcode = '', host = 'localhost', port = 61613, reliable = false, reconnect_delay = 5, connect_headers = {})
47
86
  Connection.new(login, passcode, host, port, reliable, reconnect_delay, connect_headers)
@@ -50,30 +89,158 @@ module Stomp
50
89
  def socket
51
90
  # Need to look into why the following synchronize does not work.
52
91
  #@read_semaphore.synchronize do
92
+
53
93
  s = @socket;
94
+
95
+ s = nil unless connected?
96
+
54
97
  while s.nil? || !@failure.nil?
55
98
  @failure = nil
56
99
  begin
57
- s = TCPSocket.open @host, @port
58
- headers = @connect_headers.clone
59
- headers[:login] = @login
60
- headers[:passcode] = @passcode
100
+ s = open_socket
101
+ @closed = false
102
+
103
+ headers = @connect_headers.clone
104
+ headers[:login] = @login
105
+ headers[:passcode] = @passcode
61
106
  _transmit(s, "CONNECT", headers)
62
107
  @connect = _receive(s)
63
108
  # replay any subscriptions.
64
109
  @subscriptions.each { |k,v| _transmit(s, "SUBSCRIBE", v) }
110
+
111
+ @connection_attempts = 0
65
112
  rescue
66
113
  @failure = $!;
67
114
  s=nil;
68
115
  raise unless @reliable
69
- $stderr.print "connect failed: " + $! +" will retry in #{@reconnect_delay}\n";
116
+ $stderr.print "connect to #{@host} failed: " + $! +" will retry(##{@connection_attempts}) in #{@reconnect_delay}\n";
117
+
118
+ raise "Max number of reconnection attempts reached" if max_reconnect_attempts?
119
+
70
120
  sleep(@reconnect_delay);
121
+
122
+ @connection_attempts += 1
123
+
124
+ if @parameters
125
+ change_host
126
+ increase_reconnect_delay
127
+ end
71
128
  end
72
129
  end
73
130
  @socket = s
74
131
  return s;
75
132
  #end
76
133
  end
134
+
135
+ def connected?
136
+ begin
137
+ test_socket = TCPSocket.open @host, @port
138
+ test_socket.close
139
+ open?
140
+ rescue
141
+ false
142
+ end
143
+ end
144
+
145
+ def close_socket
146
+ begin
147
+ @socket.close
148
+ rescue
149
+ #Ignoring if already closed
150
+ end
151
+
152
+ @closed = true
153
+ end
154
+
155
+ def open_socket
156
+ return TCPSocket.open @host, @port unless @ssl
157
+
158
+ ssl_socket
159
+ end
160
+
161
+ def ssl_socket
162
+ require 'openssl' unless defined?(OpenSSL)
163
+
164
+ ctx = OpenSSL::SSL::SSLContext.new
165
+
166
+ # For client certificate authentication:
167
+ # key_path = ENV["STOMP_KEY_PATH"] || "~/stomp_keys"
168
+ # ctx.cert = OpenSSL::X509::Certificate.new("#{key_path}/client.cer")
169
+ # ctx.key = OpenSSL::PKey::RSA.new("#{key_path}/client.keystore")
170
+
171
+ # For server certificate authentication:
172
+ # truststores = OpenSSL::X509::Store.new
173
+ # truststores.add_file("#{key_path}/client.ts")
174
+ # ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER
175
+ # ctx.cert_store = truststores
176
+
177
+ ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
178
+
179
+ tcp_socket = TCPSocket.new @host, @port
180
+ ssl = OpenSSL::SSL::SSLSocket.new(tcp_socket, ctx)
181
+ ssl.connect
182
+ ssl
183
+ end
184
+
185
+ def refine_params(params)
186
+ params = uncamelized_sym_keys(params)
187
+
188
+ {
189
+ :initial_reconnect_delay => 0.01,
190
+ :max_reconnect_delay => 30.0,
191
+ :use_exponential_back_off => true,
192
+ :back_off_multiplier => 2,
193
+ :max_reconnect_attempts => 0,
194
+ :randomize => false,
195
+ :connect_headers => {},
196
+ :backup => false,
197
+ :timeout => -1
198
+ }.merge(params)
199
+
200
+ end
201
+
202
+ def uncamelized_sym_keys(params)
203
+ uncamelized = {}
204
+ params.each_pair do |key, value|
205
+ key = key.to_s.split(/(?=[A-Z])/).join('_').downcase.to_sym
206
+ uncamelized[key] = value
207
+ end
208
+
209
+ uncamelized
210
+ end
211
+
212
+ def change_host
213
+ @parameters[:hosts].shuffle! if @parameters[:randomize]
214
+
215
+ # Set first as master and send it to the end of array
216
+ current_host = @parameters[:hosts].shift
217
+ @parameters[:hosts] << current_host
218
+
219
+ @ssl = current_host[:ssl]
220
+ @host = current_host[:host]
221
+ @port = current_host[:port] || default_port(@ssl)
222
+ @login = current_host[:login] || ""
223
+ @passcode = current_host[:passcode] || ""
224
+
225
+ end
226
+
227
+ def default_port(ssl)
228
+ return 61612 if ssl
229
+
230
+ 61613
231
+ end
232
+
233
+ def max_reconnect_attempts?
234
+ !(@parameters.nil? || @parameters[:max_reconnect_attempts].nil?) && @parameters[:max_reconnect_attempts] != 0 && @connection_attempts > @parameters[:max_reconnect_attempts]
235
+ end
236
+
237
+ def increase_reconnect_delay
238
+
239
+ @reconnect_delay *= @parameters[:back_off_multiplier] if @parameters[:use_exponential_back_off]
240
+ @reconnect_delay = @parameters[:max_reconnect_delay] if @reconnect_delay > @parameters[:max_reconnect_delay]
241
+
242
+ @reconnect_delay
243
+ end
77
244
 
78
245
  # Is this connection open?
79
246
  def open?
@@ -145,7 +312,8 @@ module Stomp
145
312
  # Close this connection
146
313
  def disconnect(headers = {})
147
314
  transmit("DISCONNECT", headers)
148
- @closed = true
315
+
316
+ close_socket
149
317
  end
150
318
 
151
319
  # Return a pending message if one is available, otherwise
@@ -233,7 +401,7 @@ module Stomp
233
401
  rescue
234
402
  @failure = $!;
235
403
  raise unless @reliable
236
- $stderr.print "transmit failed: " + $!+"\n";
404
+ $stderr.print "transmit to #{@host} failed: " + $!+"\n";
237
405
  end
238
406
  end
239
407
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stomp
3
3
  version: !ruby/object:Gem::Version
4
- version: "1.1"
4
+ version: 1.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian McCallister
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2009-02-27 00:00:00 +01:00
13
+ date: 2009-12-03 00:00:00 -02:00
14
14
  default_executable:
15
15
  dependencies: []
16
16
 
@@ -41,6 +41,8 @@ files:
41
41
  - test/test_helper.rb
42
42
  has_rdoc: true
43
43
  homepage: http://stomp.codehaus.org/
44
+ licenses: []
45
+
44
46
  post_install_message:
45
47
  rdoc_options:
46
48
  - --quiet
@@ -69,9 +71,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
69
71
  requirements: []
70
72
 
71
73
  rubyforge_project:
72
- rubygems_version: 1.3.1
74
+ rubygems_version: 1.3.5
73
75
  signing_key:
74
- specification_version: 2
76
+ specification_version: 3
75
77
  summary: Ruby client for the Stomp messaging protocol
76
78
  test_files:
77
79
  - test/test_client.rb