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.
- data/lib/resilient_socket/tcp_client.rb +103 -29
- data/lib/resilient_socket/version.rb +1 -1
- data/resilient_socket-0.2.0.gem +0 -0
- data/resilient_socket-0.2.1.gem +0 -0
- data/test.log +0 -0
- data/test/tcp_client_test.rb +30 -15
- metadata +4 -2
@@ -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
|
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
|
-
|
232
|
-
@
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
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
|
-
|
306
|
-
|
307
|
-
ready =
|
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
|
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
|
|
Binary file
|
Binary file
|
data/test.log
CHANGED
Binary file
|
data/test/tcp_client_test.rb
CHANGED
@@ -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
|
-
|
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.
|
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-
|
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
|