onstomp 1.0.3 → 1.0.4

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.
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