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