resilient_socket 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -42,14 +42,17 @@ module ResilientSocket
42
42
  #
43
43
  class TCPClient
44
44
  # Supports embedding user supplied data along with this connection
45
- # such as sequence number, etc.
45
+ # such as sequence number and other connection specific information
46
46
  attr_accessor :user_data
47
47
 
48
48
  # Returns [String] Name of the server connected to including the port number
49
49
  #
50
50
  # Example:
51
51
  # localhost:2000
52
- attr_reader :server
52
+ attr_reader :server, :buffered
53
+
54
+ attr_accessor :read_timeout, :connect_timeout, :connect_retry_count,
55
+ :retry_count, :connect_retry_interval, :server_selector, :close_on_error
53
56
 
54
57
  # Returns [TrueClass|FalseClass] Whether send buffering is enabled for this connection
55
58
  attr_reader :buffered
@@ -163,6 +166,31 @@ module ResilientSocket
163
166
  # - Pass any authentication information to the server
164
167
  # - Perform a handshake with the server
165
168
  #
169
+ # :server_selector [Symbol|Proc]
170
+ # When multiple servers are supplied using :servers, this option will
171
+ # determine which server is selected from the list
172
+ # :ordered
173
+ # Select a server in the order supplied in the array, with the first
174
+ # having the highest priority. The second server will only be connected
175
+ # to if the first server is unreachable
176
+ # :random
177
+ # Randomly select a server from the list every time a connection
178
+ # is established, including during automatic connection recovery.
179
+ # Proc:
180
+ # When a Proc is supplied, it will be called passing in the list
181
+ # of servers. The Proc must return one server name
182
+ # Example:
183
+ # :server_selector => Proc.new do |servers|
184
+ # servers.last
185
+ # end
186
+ # Default: :ordered
187
+ #
188
+ # :close_on_error [True|False]
189
+ # To prevent the connection from going into an inconsistent state
190
+ # automatically close the connection if an error occurs
191
+ # This includes a Read Timeout
192
+ # Default: true
193
+ #
166
194
  # Example
167
195
  # client = ResilientSocket::TCPClient.new(
168
196
  # :server => 'server:3300',
@@ -189,6 +217,9 @@ module ResilientSocket
189
217
  @retry_count = params.delete(:retry_count) || 3
190
218
  @connect_retry_interval = (params.delete(:connect_retry_interval) || 0.5).to_f
191
219
  @on_connect = params.delete(:on_connect)
220
+ @server_selector = params.delete(:server_selector) || :ordered
221
+ @close_on_error = params.delete(:close_on_error)
222
+ @close_on_error = true if @close_on_error.nil?
192
223
 
193
224
  unless @servers = params.delete(:servers)
194
225
  raise "Missing mandatory :server or :servers" unless server = params.delete(:server)
@@ -228,14 +259,46 @@ module ResilientSocket
228
259
  def connect
229
260
  @socket.close if @socket && !@socket.closed?
230
261
  if @servers.size > 1
231
- # Try each server in sequence
232
- @servers.each_with_index do |server, server_id|
233
- begin
234
- connect_to_server(server)
235
- rescue ConnectionFailure => exc
236
- # Raise Exception once it has also failed to connect to the last server
237
- raise(exc) if @servers.size <= (server_id + 1)
262
+ case
263
+ when @server_selector.is_a?(Proc)
264
+ connect_to_server(@server_selector.call(@servers))
265
+
266
+ when @server_selector == :ordered
267
+ # Try each server in sequence
268
+ exception = nil
269
+ @servers.find do |server|
270
+ begin
271
+ connect_to_server(server)
272
+ exception = nil
273
+ true
274
+ rescue ConnectionFailure => exc
275
+ exception = exc
276
+ false
277
+ end
278
+ end
279
+ # Raise Exception once it has also failed to connect to all servers
280
+ raise(exception) if exception
281
+
282
+ when @server_selector == :random
283
+ # Pick each server randomly, trying each server until one can be connected to
284
+ # If no server can be connected to a ConnectionFailure is raised
285
+ servers_to_try = @servers.uniq
286
+ exception = nil
287
+ servers_to_try.size.times do |i|
288
+ server = servers_to_try[rand(servers_to_try.size)]
289
+ servers_to_try.delete(server)
290
+ begin
291
+ connect_to_server(server)
292
+ exception = nil
293
+ rescue ConnectionFailure => exc
294
+ exception = exc
295
+ end
238
296
  end
297
+ # Raise Exception once it has also failed to connect to all servers
298
+ raise(exception) if exception
299
+
300
+ else
301
+ raise ArgumentError.new("Invalid or unknown value for parameter :server_selector => #{@server_selector}")
239
302
  end
240
303
  else
241
304
  connect_to_server(@servers.first)
@@ -260,8 +323,13 @@ module ResilientSocket
260
323
  @socket.write(data)
261
324
  rescue SystemCallError => exception
262
325
  @logger.warn "#write Connection failure: #{exception.class}: #{exception.message}"
263
- close
326
+ close if close_on_error
264
327
  raise ConnectionFailure.new("Send Connection failure: #{exception.class}: #{exception.message}", @server, exception)
328
+ rescue Exception
329
+ # Close the connection on any other exception since the connection
330
+ # will now be in an inconsistent state
331
+ close if close_on_error
332
+ raise
265
333
  end
266
334
  end
267
335
  end
@@ -290,6 +358,7 @@ module ResilientSocket
290
358
  # Optional: Override the default read timeout for this read
291
359
  # Number of seconds before raising ReadTimeout when no data has
292
360
  # been returned
361
+ # A value of -1 will wait forever for a response on the socket
293
362
  # Default: :read_timeout supplied to #initialize
294
363
  #
295
364
  # Note: After a ResilientSocket::ReadTimeout #read can be called again on
@@ -302,17 +371,25 @@ module ResilientSocket
302
371
  def read(length, buffer=nil, timeout=nil)
303
372
  result = nil
304
373
  @logger.benchmark_debug("#read <== read #{length} bytes") do
305
- # Block on data to read for @read_timeout seconds
306
- begin
307
- ready = IO.select([@socket], nil, [@socket], timeout || @read_timeout)
374
+ if timeout != -1
375
+ # Block on data to read for @read_timeout seconds
376
+ ready = begin
377
+ ready = IO.select([@socket], nil, [@socket], timeout || @read_timeout)
378
+ rescue IOError => exception
379
+ @logger.warn "#read Connection failure while waiting for data: #{exception.class}: #{exception.message}"
380
+ close if close_on_error
381
+ raise ConnectionFailure.new("#{exception.class}: #{exception.message}", @server, exception)
382
+ rescue Exception
383
+ # Close the connection on any other exception since the connection
384
+ # will now be in an inconsistent state
385
+ close if close_on_error
386
+ raise
387
+ end
308
388
  unless ready
389
+ close if close_on_error
309
390
  @logger.warn "#read Timeout waiting for server to reply"
310
391
  raise ReadTimeout.new("Timedout after #{timeout || @read_timeout} seconds trying to read from #{@server}")
311
392
  end
312
- rescue IOError => exception
313
- @logger.warn "#read Connection failure while waiting for data: #{exception.class}: #{exception.message}"
314
- close
315
- raise ConnectionFailure.new("#{exception.class}: #{exception.message}", @server, exception)
316
393
  end
317
394
 
318
395
  # Read data from socket
@@ -322,14 +399,19 @@ module ResilientSocket
322
399
 
323
400
  # EOF before all the data was returned
324
401
  if result.nil? || (result.length < length)
325
- close
402
+ close if close_on_error
326
403
  @logger.warn "#read server closed the connection before #{length} bytes were returned"
327
404
  raise ConnectionFailure.new("Connection lost while reading data", @server, EOFError.new("end of file reached"))
328
405
  end
329
406
  rescue SystemCallError, IOError => exception
330
- close
407
+ close if close_on_error
331
408
  @logger.warn "#read Connection failure while reading data: #{exception.class}: #{exception.message}"
332
409
  raise ConnectionFailure.new("#{exception.class}: #{exception.message}", @server, exception)
410
+ rescue Exception
411
+ # Close the connection on any other exception since the connection
412
+ # will now be in an inconsistent state
413
+ close if close_on_error
414
+ raise
333
415
  end
334
416
  end
335
417
  result
@@ -337,8 +419,9 @@ module ResilientSocket
337
419
 
338
420
  # Send and/or receive data with automatic retry on connection failure
339
421
  #
340
- # On a connection failure, it will close the connection and retry the block
422
+ # On a connection failure, it will create a new connection and retry the block.
341
423
  # Returns immediately on exception ReadTimeout
424
+ # The connection is always closed on ConnectionFailure regardless of close_on_error
342
425
  #
343
426
  # 1. Example of a resilient _readonly_ request:
344
427
  #
@@ -381,10 +464,6 @@ module ResilientSocket
381
464
  connect if closed?
382
465
  yield(self)
383
466
  rescue ConnectionFailure => exception
384
- # Connection no longer usable. The next call to #retry_on_connection_failure
385
- # will create a new connection since this one is now closed
386
- close
387
-
388
467
  exc_str = exception.cause ? "#{exception.cause.class}: #{exception.cause.message}" : exception.message
389
468
  # Re-raise exceptions that should not be retried
390
469
  if !self.class.reconnect_on_errors.include?(exception.cause.class)
@@ -398,11 +477,6 @@ module ResilientSocket
398
477
  end
399
478
  @logger.error "#retry_on_connection_failure Connection failure: #{exception.class}: #{exception.message}. Giving up after #{retries} retries"
400
479
  raise ConnectionFailure.new("After #{retries} retries to host '#{server}': #{exc_str}", @server, exception.cause)
401
- rescue Exception => exc
402
- # With any other exception we have to close the connection since the connection
403
- # is now in an unknown state
404
- close
405
- raise exc
406
480
  end
407
481
  end
408
482
 
@@ -1,3 +1,3 @@
1
1
  module ResilientSocket #:nodoc
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
data/test.log CHANGED
Binary file
@@ -51,6 +51,32 @@ class TCPClientTest < Test::Unit::TestCase
51
51
  # assert_match /Timedout after/, exception.message
52
52
  #end
53
53
 
54
+ context "without client connection" do
55
+ should "timeout on first receive and then successfully read the response" do
56
+ @read_timeout = 3.0
57
+ # Need a custom client that does not auto close on error:
58
+ @client = ResilientSocket::TCPClient.new(
59
+ :server => @server_name,
60
+ :read_timeout => @read_timeout,
61
+ :close_on_error => false
62
+ )
63
+
64
+ request = { 'action' => 'sleep', 'duration' => @read_timeout + 0.5}
65
+ @client.write(BSON.serialize(request))
66
+
67
+ exception = assert_raise ResilientSocket::ReadTimeout do
68
+ # Read 4 bytes from server
69
+ @client.read(4)
70
+ end
71
+ assert_equal false, @client.close_on_error
72
+ assert @client.alive?, "The client connection is not alive after the read timed out with :close_on_error => false"
73
+ assert_match /Timedout after #{@read_timeout} seconds trying to read from #{@server_name}/, exception.message
74
+ reply = read_bson_document(@client)
75
+ assert_equal 'sleep', reply['result']
76
+ @client.close
77
+ end
78
+ end
79
+
54
80
  context "with client connection" do
55
81
  setup do
56
82
  @read_timeout = 3.0
@@ -59,6 +85,7 @@ class TCPClientTest < Test::Unit::TestCase
59
85
  :read_timeout => @read_timeout
60
86
  )
61
87
  assert @client.alive?
88
+ assert_equal true, @client.close_on_error
62
89
  end
63
90
 
64
91
  def teardown
@@ -83,24 +110,12 @@ class TCPClientTest < Test::Unit::TestCase
83
110
  # Read 4 bytes from server
84
111
  @client.read(4)
85
112
  end
86
- assert @client.alive?
113
+ # Due to :close_on_error => true, a timeout will close the connection
114
+ # to prevent use of a socket connection in an inconsistent state
115
+ assert_equal false, @client.alive?
87
116
  assert_match /Timedout after #{@read_timeout} seconds trying to read from #{@server_name}/, exception.message
88
117
  end
89
118
 
90
- should "timeout on first receive and then successfully read the response" do
91
- request = { 'action' => 'sleep', 'duration' => @read_timeout + 0.5}
92
- @client.write(BSON.serialize(request))
93
-
94
- exception = assert_raise ResilientSocket::ReadTimeout do
95
- # Read 4 bytes from server
96
- @client.read(4)
97
- end
98
- assert @client.alive?
99
- assert_match /Timedout after #{@read_timeout} seconds trying to read from #{@server_name}/, exception.message
100
- reply = read_bson_document(@client)
101
- assert_equal 'sleep', reply['result']
102
- end
103
-
104
119
  should "retry on connection failure" do
105
120
  attempt = 0
106
121
  reply = @client.retry_on_connection_failure do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: resilient_socket
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-10-15 00:00:00.000000000 Z
12
+ date: 2012-10-18 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: semantic_logger
@@ -47,6 +47,8 @@ files:
47
47
  - nbproject/project.xml
48
48
  - Rakefile
49
49
  - README.md
50
+ - resilient_socket-0.2.0.gem
51
+ - resilient_socket-0.2.1.gem
50
52
  - test/simple_tcp_server.rb
51
53
  - test/tcp_client_test.rb
52
54
  - test.log