onstomp 1.0.3 → 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/.rspec CHANGED
@@ -1,2 +1,2 @@
1
1
  --colour
2
- --format d
2
+ --format p
@@ -1,5 +1,15 @@
1
1
  # Changes
2
2
 
3
+ ## 1.0.4
4
+ * fixed major oversight when using ruby 1.8.7 / jruby with an SSL connection.
5
+ it will use blocking read/write methods if nonblocking methods are unavailable
6
+ * added write and read block timeouts. if there is no write activity for a period
7
+ of time (default of 120 seconds), the connection is assumed to be dead. read
8
+ activity is handled similarly, but only when performing the CONNECT/CONNECTED
9
+ exchange, after that we have no way of knowing if the broker just doesn't
10
+ have any data for us (unless using a STOMP 1.1 connection with heartbeating)
11
+ * refactored common failover buffer code into lib/onstomp/failover/buffers/base
12
+
3
13
  ## 1.0.3
4
14
  * change how failover spawns new connections when failing over.
5
15
 
data/Gemfile CHANGED
@@ -3,7 +3,11 @@ source "http://rubygems.org"
3
3
  # Specify your gem's dependencies in onstomp.gemspec
4
4
  gemspec
5
5
 
6
- group :development do
6
+ group :docs do
7
7
  gem 'rdiscount'
8
8
  gem 'erubis'
9
9
  end
10
+
11
+ platform :jruby do
12
+ gem 'jruby-openssl'
13
+ end
@@ -47,6 +47,17 @@ class OnStomp::Client
47
47
  # @return [Class]
48
48
  attr_configurable_processor :processor
49
49
 
50
+ # The number of seconds to wait before a write-blocked connection is
51
+ # considered dead. Defaults to 120 seconds.
52
+ # @return [Fixnum]
53
+ attr_configurable_int :write_timeout, :default => 120
54
+
55
+ # The number of seconds to wait before a connection that is read-blocked
56
+ # during the {OnStomp::Connections::Base#connect connect} phase is
57
+ # considered dead. Defaults to 120 seconds.
58
+ # @return [Fixnum]
59
+ attr_configurable_int :read_timeout, :default => 120
60
+
50
61
  # @api gem:1 STOMP:1.0,1.1
51
62
  # Creates a new client for the specified uri and optional hash of options.
52
63
  # @param [String,URI] uri
@@ -71,7 +82,8 @@ class OnStomp::Client
71
82
  @connection = OnStomp::Connections.connect self, headers,
72
83
  { :'accept-version' => @versions.join(','), :host => @host,
73
84
  :'heart-beat' => @heartbeats.join(','), :login => @login,
74
- :passcode => @passcode }, pending_connection_events
85
+ :passcode => @passcode }, pending_connection_events,
86
+ read_timeout, write_timeout
75
87
  processor_inst.start
76
88
  self
77
89
  end
@@ -44,8 +44,8 @@ module OnStomp::Connections
44
44
  # negotiated protocol version
45
45
  # @raise [OnStomp::OnStompError] if negotiating the connection raises an
46
46
  # such an error.
47
- def self.connect client, u_head, c_head, con_cbs
48
- init_con = create_connection('1.0', nil, client)
47
+ def self.connect client, u_head, c_head, con_cbs, r_time, w_time
48
+ init_con = create_connection('1.0', nil, client, r_time, w_time)
49
49
  ver, connected = init_con.connect client, u_head, c_head
50
50
  begin
51
51
  negotiate_connection(ver, init_con, client).tap do |final_con|
@@ -61,21 +61,25 @@ module OnStomp::Connections
61
61
  private
62
62
  def self.negotiate_connection vers, con, client
63
63
  supports_protocol?(vers,con) ? con :
64
- create_connection(vers, con.socket, client)
64
+ create_connection(vers, con.socket, client, con.read_timeout,
65
+ con.write_timeout)
65
66
  end
66
67
 
67
68
  def self.supports_protocol? ver, con
68
69
  con.is_a? PROTOCOL_VERSIONS[ver]
69
70
  end
70
71
 
71
- def self.create_connection ver, sock, client
72
+ def self.create_connection ver, sock, client, rt, wt
72
73
  unless sock
73
74
  meth = client.ssl ? :ssl :
74
75
  client.uri.respond_to?(:onstomp_socket_type) ?
75
76
  client.uri.onstomp_socket_type : :tcp
76
77
  sock = __send__(:"create_socket_#{meth}", client)
77
78
  end
78
- PROTOCOL_VERSIONS[ver].new sock, client
79
+ PROTOCOL_VERSIONS[ver].new(sock, client).tap do |con|
80
+ con.read_timeout = rt
81
+ con.write_timeout = wt
82
+ end
79
83
  end
80
84
 
81
85
  def self.create_socket_tcp client
@@ -5,6 +5,7 @@ class OnStomp::Connections::Base
5
5
  include OnStomp::Interfaces::ConnectionEvents
6
6
  attr_reader :version, :socket, :client
7
7
  attr_reader :last_transmitted_at, :last_received_at
8
+ attr_accessor :write_timeout, :read_timeout
8
9
 
9
10
  # The approximate maximum number of bytes to write per call to
10
11
  # {#io_process_write}.
@@ -26,6 +27,9 @@ class OnStomp::Connections::Base
26
27
  @read_buffer = []
27
28
  @client = client
28
29
  @connection_up = false
30
+ @write_timeout = nil
31
+ @read_timeout = nil
32
+ setup_non_blocking_methods
29
33
  end
30
34
 
31
35
  # Performs any necessary configuration of the connection from the CONNECTED
@@ -71,9 +75,10 @@ class OnStomp::Connections::Base
71
75
  until client_con
72
76
  io_process_write { |f| client_con ||= f }
73
77
  end
78
+ @last_received_at = Time.now
74
79
  broker_con = nil
75
80
  until broker_con
76
- io_process_read { |f| broker_con ||= f }
81
+ io_process_read(true) { |f| broker_con ||= f }
77
82
  end
78
83
  raise OnStomp::ConnectFailedError if broker_con.command != 'CONNECTED'
79
84
  vers = broker_con.header?(:version) ? broker_con[:version] : '1.0'
@@ -93,6 +98,20 @@ class OnStomp::Connections::Base
93
98
  end
94
99
  end
95
100
 
101
+ # Number of milliseconds since data was last transmitted to the broker or
102
+ # `nil` if no data has been transmitted when the method is called.
103
+ # @return [Fixnum, nil]
104
+ def duration_since_transmitted
105
+ last_transmitted_at && ((Time.now - last_transmitted_at)*1000).to_i
106
+ end
107
+
108
+ # Number of milliseconds since data was last received from the broker or
109
+ # `nil` if no data has been received when the method is called.
110
+ # @return [Fixnum, nil]
111
+ def duration_since_received
112
+ last_received_at && ((Time.now - last_received_at)*1000).to_i
113
+ end
114
+
96
115
  # Flushes the write buffer by invoking {#io_process_write} until the
97
116
  # buffer is empty.
98
117
  def flush_write_buffer
@@ -122,6 +141,7 @@ class OnStomp::Connections::Base
122
141
  # @param [OnStomp::Components::Frame]
123
142
  def push_write_buffer data, frame
124
143
  @write_mutex.synchronize {
144
+ @last_write_activity = Time.now if @write_buffer.empty?
125
145
  @write_buffer << [data, frame] unless @closing
126
146
  }
127
147
  end
@@ -146,34 +166,34 @@ class OnStomp::Connections::Base
146
166
  # sent to the head of the write buffer to be processed first the next time
147
167
  # this method is invoked.
148
168
  def io_process_write
149
- begin
150
- if @write_buffer.length > 0 && IO.select(nil, [socket], nil, 0.1)
151
- to_shift = @write_buffer.length / 3
152
- written = 0
153
- while written < MAX_BYTES_PER_WRITE
154
- data, frame = shift_write_buffer
155
- break unless data && connected?
156
- begin
157
- w = socket.write_nonblock(data)
158
- rescue Errno::EINTR, Errno::EAGAIN, Errno::EWOULDBLOCK
159
- # writing will either block, or cannot otherwise be completed,
160
- # put data back and try again some other day
161
- unshift_write_buffer data, frame
162
- break
163
- end
164
- written += w
165
- @last_transmitted_at = Time.now
166
- if w < data.length
167
- unshift_write_buffer data[w..-1], frame
168
- else
169
- yield frame if block_given?
170
- client.dispatch_transmitted frame
171
- end
169
+ if ready_for_write?
170
+ to_shift = @write_buffer.length / 3
171
+ written = 0
172
+ while written < MAX_BYTES_PER_WRITE
173
+ data, frame = shift_write_buffer
174
+ break unless data && connected?
175
+ begin
176
+ w = write_nonblock data
177
+ rescue Errno::EINTR, Errno::EAGAIN, Errno::EWOULDBLOCK
178
+ # writing will either block, or cannot otherwise be completed,
179
+ # put data back and try again some other day
180
+ unshift_write_buffer data, frame
181
+ break
182
+ rescue Exception
183
+ triggered_close $!.message, :terminated
184
+ raise
185
+ end
186
+ written += w
187
+ @last_write_activity = @last_transmitted_at = Time.now
188
+ if w < data.length
189
+ unshift_write_buffer data[w..-1], frame
190
+ else
191
+ yield frame if block_given?
192
+ client.dispatch_transmitted frame
172
193
  end
173
194
  end
174
- rescue Exception
175
- triggered_close $!.message, :terminated
176
- raise
195
+ elsif write_timeout_exceeded?
196
+ triggered_close 'write blocked', :blocked
177
197
  end
178
198
  if @write_buffer.empty? && @closing
179
199
  triggered_close 'client disconnected'
@@ -184,37 +204,123 @@ class OnStomp::Connections::Base
184
204
  # and the socket is ready for reading. The received data will be pushed
185
205
  # to the end of a read buffer, which is then sent to the connection's
186
206
  # {OnStomp::Connections::Serializers serializer} for processing.
187
- def io_process_read
188
- begin
189
- if connected? && IO.select([socket], nil, nil, 0.1)
190
- if data = socket.read_nonblock(MAX_BYTES_PER_READ)
207
+ def io_process_read(check_timeout=false)
208
+ if ready_for_read?
209
+ begin
210
+ if data = read_nonblock
191
211
  @read_buffer << data
192
212
  @last_received_at = Time.now
193
213
  serializer.bytes_to_frame(@read_buffer) do |frame|
194
214
  yield frame if block_given?
195
215
  client.dispatch_received frame
196
216
  end
197
- else
198
- triggered_close $!.message, :terminated
199
217
  end
218
+ rescue Errno::EINTR, Errno::EAGAIN, Errno::EWOULDBLOCK
219
+ # do not
220
+ rescue EOFError
221
+ triggered_close $!.message
222
+ rescue Exception
223
+ triggered_close $!.message, :terminated
224
+ raise
200
225
  end
201
- rescue Errno::EINTR, Errno::EAGAIN, Errno::EWOULDBLOCK
202
- # do not
203
- rescue EOFError
204
- triggered_close $!.message
226
+ elsif check_timeout && read_timeout_exceeded?
227
+ triggered_close 'read blocked', :blocked
228
+ end
229
+ end
230
+
231
+ private
232
+ def duration_since_write_activity
233
+ Time.now - @last_write_activity
234
+ end
235
+
236
+ # Returns true if the connection has buffered data to write and the
237
+ # socket is ready to be written to. If checking the socket's state raises
238
+ # an exception, the connection will be closed (triggering an
239
+ # `on_terminated` event) and the error will be re-raised.
240
+ def ready_for_write?
241
+ begin
242
+ @write_buffer.length > 0 && IO.select(nil, [socket], nil, 0.1)
205
243
  rescue Exception
206
244
  triggered_close $!.message, :terminated
207
245
  raise
208
246
  end
209
247
  end
210
248
 
211
- private
249
+ # Returns true if the connection has buffered data to write and the
250
+ # socket is ready to be written to. If checking the socket's state raises
251
+ # an exception, the connection will be closed (triggering an
252
+ # `on_terminated` event) and the error will be re-raised.
253
+ def ready_for_read?
254
+ begin
255
+ connected? && IO.select([socket], nil, nil, 0.1)
256
+ rescue Exception
257
+ triggered_close $!.message, :terminated
258
+ raise
259
+ end
260
+ end
261
+
262
+ # Returns true if a `write_timeout` has been set, the connection has buffered
263
+ # data to write, and `duration_since_transmitted` is greater than
264
+ # `write_timeout`
265
+ def write_timeout_exceeded?
266
+ @write_timeout && @write_buffer.length > 0 &&
267
+ duration_since_write_activity > @write_timeout
268
+ end
269
+
270
+ # Returns true if a `read_timeout` has been set and
271
+ # `duration_since_received` is greater than `read_timeout`
272
+ # This is only used when establishing the connection through the CONNECT/
273
+ # CONNECTED handshake. After that, it is up to heart-beating.
274
+ def read_timeout_exceeded?
275
+ @read_timeout && duration_since_received > (@read_timeout*1000)
276
+ end
277
+
212
278
  def triggered_close msg, *evs
213
279
  @connection_up = false
214
280
  @closing = false
281
+ # unless socket.closed?
282
+ # socket.to_io.shutdown(2) rescue nil
283
+ #
284
+ # end
215
285
  socket.close rescue nil
216
286
  evs.each { |ev| trigger_connection_event ev, msg }
217
287
  trigger_connection_event :closed, msg
218
288
  @write_buffer.clear
219
289
  end
290
+
291
+ # OpenSSL sockets in Ruby 1.8.7 and JRuby (as of jruby-openssl 0.7.3)
292
+ # do NOT support non-blocking IO natively. Such a hack, and such a huge
293
+ # oversight on my part. We define some methods on this instance to use
294
+ # the right read/write operations. Fortunately, this gets done at
295
+ # initialization and only has to happen once.
296
+ def setup_non_blocking_methods
297
+ read_mod = @socket.respond_to?(:read_nonblock) ? NonblockingRead :
298
+ BlockingRead
299
+ write_mod = @socket.respond_to?(:write_nonblock) ? NonblockingWrite :
300
+ BlockingWrite
301
+ self.extend read_mod
302
+ self.extend write_mod
303
+ end
304
+
305
+ module NonblockingRead
306
+ def read_nonblock
307
+ socket.read_nonblock MAX_BYTES_PER_READ
308
+ end
309
+ end
310
+ module NonblockingWrite
311
+ def write_nonblock data
312
+ socket.write_nonblock data
313
+ end
314
+ end
315
+
316
+ module BlockingRead
317
+ def read_nonblock
318
+ socket.readpartial MAX_BYTES_PER_READ
319
+ end
320
+ end
321
+ module BlockingWrite
322
+ def write_nonblock data
323
+ socket.write data
324
+ end
325
+ end
220
326
  end
@@ -51,20 +51,6 @@ module OnStomp::Connections::Heartbeating
51
51
  end
52
52
  @heartbeat_broker_limit
53
53
  end
54
-
55
- # Number of milliseconds since data was last transmitted to the broker or
56
- # `nil` if no data has been transmitted when the method is called.
57
- # @return [Fixnum, nil]
58
- def duration_since_transmitted
59
- last_transmitted_at && ((Time.now - last_transmitted_at)*1000).to_i
60
- end
61
-
62
- # Number of milliseconds since data was last received from the broker or
63
- # `nil` if no data has been received when the method is called.
64
- # @return [Fixnum, nil]
65
- def duration_since_received
66
- last_received_at && ((Time.now - last_received_at)*1000).to_i
67
- end
68
54
 
69
55
  # Returns true if client-side heartbeating is disabled, or
70
56
  # {#duration_since_transmitted} has not exceeded {#heartbeat_client_limit}
@@ -5,5 +5,6 @@
5
5
  module OnStomp::Failover::Buffers
6
6
  end
7
7
 
8
+ require 'onstomp/failover/buffers/base'
8
9
  require 'onstomp/failover/buffers/written'
9
10
  require 'onstomp/failover/buffers/receipts'
@@ -0,0 +1,59 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # The base class for all buffers. This class exists mostly as a factoring
4
+ # out of the code shared between the {OnStomp::Failover::Buffers::Written}
5
+ # and {OnStomp::Failover::Buffers::Receipts} buffers.
6
+ class OnStomp::Failover::Buffers::Base
7
+ def initialize failover
8
+ @failover = failover
9
+ @buffer_mutex = Mutex.new
10
+ @buffer = []
11
+ @txs = {}
12
+ end
13
+
14
+ # Returns the number of frames currently sitting in the buffer.
15
+ # @return [Fixnum]
16
+ def buffered
17
+ @buffer.length
18
+ end
19
+
20
+ private
21
+ def add_to_buffer f, heads={}
22
+ @buffer_mutex.synchronize do
23
+ unless f.header? :'x-onstomp-failover-replay'
24
+ f.headers.reverse_merge! heads
25
+ @buffer << f
26
+ end
27
+ end
28
+ end
29
+
30
+ def add_to_transactions f, heads={}
31
+ @txs[f[:transaction]] = true
32
+ add_to_buffer f, heads
33
+ end
34
+
35
+ def remove_from_transactions f
36
+ tx = f[:transaction]
37
+ if @txs.delete tx
38
+ @buffer_mutex.synchronize do
39
+ @buffer.reject! { |bf| bf[:transaction] == tx }
40
+ end
41
+ end
42
+ end
43
+
44
+ def remove_subscribe_from_buffer f
45
+ @buffer_mutex.synchronize do
46
+ @buffer.reject! { |bf| bf.command == 'SUBSCRIBE' && bf[:id] == f[:id] }
47
+ end
48
+ end
49
+
50
+ def replay_buffer client
51
+ replay_frames = @buffer_mutex.synchronize do
52
+ @buffer.select { |f| f[:'x-onstomp-failover-replay'] = '1'; true }
53
+ end
54
+
55
+ replay_frames.each do |f|
56
+ client.transmit f
57
+ end
58
+ end
59
+ end
@@ -6,79 +6,38 @@
6
6
  # {OnStomp::Failover::Client failover} client reconnects.
7
7
  # @todo Quite a lot of this code is shared between Written and Receipts,
8
8
  # we'll want to factor the common stuff out.
9
- class OnStomp::Failover::Buffers::Receipts
9
+ class OnStomp::Failover::Buffers::Receipts < OnStomp::Failover::Buffers::Base
10
10
  def initialize failover
11
- @failover = failover
12
- @buffer_mutex = Mutex.new
13
- @buffer = []
14
- @txs = {}
15
-
16
- failover.before_send &method(:buffer_frame)
17
- failover.before_commit &method(:buffer_frame)
18
- failover.before_abort &method(:buffer_frame)
19
- failover.before_subscribe &method(:buffer_frame)
20
- failover.before_begin &method(:buffer_transaction)
11
+ super
12
+ [:send, :commit, :abort, :subscribe].each do |ev|
13
+ failover.__send__(:"before_#{ev}") do |f, *_|
14
+ add_to_buffer f, {:receipt => OnStomp.next_serial}
15
+ end
16
+ end
17
+ failover.before_begin do |f, *_|
18
+ add_to_transactions f, {:receipt => OnStomp.next_serial}
19
+ end
21
20
  # We can scrub the subscription before UNSUBSCRIBE is fully written
22
21
  # because if we replay before UNSUBSCRIBE was sent, we still don't
23
22
  # want to be subscribed when we reconnect.
24
- failover.before_unsubscribe &method(:debuffer_subscription)
25
- failover.on_receipt &method(:debuffer_frame)
26
-
27
- failover.on_failover_connected &method(:replay)
28
- end
29
-
30
- # Adds a frame to a buffer so that it may be replayed if the
31
- # {OnStomp::Failover::Client failover} client re-connects
32
- def buffer_frame f, *_
33
- @buffer_mutex.synchronize do
34
- # Don't re-buffer frames that are being replayed.
35
- unless f.header? :'x-onstomp-failover-replay'
36
- # Create a receipt header, unless the frame already has one.
37
- f[:receipt] = OnStomp.next_serial unless f.header?(:receipt)
38
- @buffer << f
39
- end
40
- end
41
- end
42
-
43
- # Records the start of a transaction so that it may be replayed if the
44
- # {OnStomp::Failover::Client failover} client re-connects
45
- def buffer_transaction f, *_
46
- @txs[f[:transaction]] = true
47
- buffer_frame f
48
- end
49
-
50
- # Removes the recorded transaction from the buffer after it has been
51
- # written the broker socket so that it will not be replayed when the
52
- # {OnStomp::Failover::Client failover} client re-connects
53
- def debuffer_transaction f
54
- tx = f[:transaction]
55
- if @txs.delete tx
56
- @buffer_mutex.synchronize do
57
- @buffer.reject! { |bf| bf[:transaction] == tx }
58
- end
23
+ failover.before_unsubscribe do |f, *_|
24
+ remove_subscribe_from_buffer f
59
25
  end
26
+ failover.on_receipt { |r, *_| debuffer_frame r }
27
+ failover.on_failover_connected { |f,c,*_| replay_buffer c }
60
28
  end
61
29
 
62
- # Removes the matching SUBSCRIBE frame from the buffer after the
63
- # UNSUBSCRIBE has been added to the connection's write buffer
64
- # so that it will not be replayed when the
65
- # {OnStomp::Failover::Client failover} client re-connects
66
- def debuffer_subscription f, *_
67
- @buffer_mutex.synchronize do
68
- @buffer.reject! { |bf| bf.command == 'SUBSCRIBE' && bf[:id] == f[:id] }
69
- end
70
- end
71
30
 
72
31
  # Removes frames that neither transactional nor SUBSCRIBEs from the buffer
73
32
  # by looking the buffered frames up by their `receipt` header.
74
- def debuffer_frame r, *_
33
+ def debuffer_frame r
75
34
  orig = @buffer_mutex.synchronize do
76
35
  @buffer.detect { |f| f[:receipt] == r[:'receipt-id'] }
77
36
  end
78
37
  if orig
79
38
  # COMMIT and ABORT debuffer the whole transaction sequence
80
39
  if ['COMMIT', 'ABORT'].include? orig.command
81
- debuffer_transaction orig
40
+ remove_from_transactions orig
82
41
  # Otherwise, if this isn't part of a transaction, debuffer the
83
42
  # particular frame (if it's not a SUBSCRIBE)
84
43
  elsif orig.command != 'SUBSCRIBE' && !orig.header?(:transaction)
@@ -86,16 +45,4 @@ class OnStomp::Failover::Buffers::Receipts
86
45
  end
87
46
  end
88
47
  end
89
-
90
- # Called when the {OnStomp::Failover::Client failover} client triggers
91
- # `on_failover_connected` to start replaying any frames in the buffer.
92
- def replay fail, client, *_
93
- replay_frames = @buffer_mutex.synchronize do
94
- @buffer.select { |f| f[:'x-onstomp-failover-replay'] = '1'; true }
95
- end
96
-
97
- replay_frames.each do |f|
98
- client.transmit f
99
- end
100
- end
101
48
  end