resilient_socket 0.2.0 → 0.3.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.
@@ -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