resilient_socket 0.0.2 → 0.1.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 +164 -89
- data/lib/resilient_socket/version.rb +1 -1
- data/nbproject/private/config.properties +0 -0
- data/nbproject/private/private.properties +1 -0
- data/nbproject/project.properties +1 -0
- data/resilient_socket-0.0.2.gem +0 -0
- data/test/tcp_client_test.rb +38 -0
- data/test.log +0 -0
- metadata +4 -2
@@ -35,13 +35,15 @@ module ResilientSocket
|
|
35
35
|
# due to a failed connection to the server
|
36
36
|
attr_accessor :user_data
|
37
37
|
|
38
|
-
# [String] Name of the server
|
39
|
-
# including the port number
|
38
|
+
# Returns [String] Name of the server connected to including the port number
|
40
39
|
#
|
41
40
|
# Example:
|
42
|
-
#
|
41
|
+
# localhost:2000
|
43
42
|
attr_reader :server
|
44
43
|
|
44
|
+
# Returns [TrueClass|FalseClass] Whether send buffering is enabled for this connection
|
45
|
+
attr_reader :buffered
|
46
|
+
|
45
47
|
# Create a connection, call the supplied block and close the connection on
|
46
48
|
# completion of the block
|
47
49
|
#
|
@@ -116,6 +118,14 @@ module ResilientSocket
|
|
116
118
|
# Number of seconds between connection retry attempts after the first failed attempt
|
117
119
|
# Default: 0.5
|
118
120
|
#
|
121
|
+
# :on_connect [Proc]
|
122
|
+
# Directly after a connection is established and before it is made available
|
123
|
+
# for use this Block is invoked.
|
124
|
+
# Typical Use Cases:
|
125
|
+
# - Initialize per connection session sequence numbers
|
126
|
+
# - Pass any authentication information to the server
|
127
|
+
# - Perform a handshake with the server
|
128
|
+
#
|
119
129
|
# Example
|
120
130
|
# client = ResilientSocket::TCPClient.new(
|
121
131
|
# :server => 'server:3300',
|
@@ -127,7 +137,9 @@ module ResilientSocket
|
|
127
137
|
# client.send('Update the database')
|
128
138
|
# end
|
129
139
|
#
|
140
|
+
# # Read upto 20 characters from the server
|
130
141
|
# response = client.read(20)
|
142
|
+
#
|
131
143
|
# puts "Received: #{response}"
|
132
144
|
# client.close
|
133
145
|
def initialize(parameters={})
|
@@ -138,8 +150,9 @@ module ResilientSocket
|
|
138
150
|
@buffered = buffered.nil? ? true : buffered
|
139
151
|
@connect_retry_count = params.delete(:connect_retry_count) || 10
|
140
152
|
@connect_retry_interval = (params.delete(:connect_retry_interval) || 0.5).to_f
|
153
|
+
@on_connect = params.delete(:on_connect)
|
141
154
|
|
142
|
-
unless @servers = params
|
155
|
+
unless @servers = params.delete(:servers)
|
143
156
|
raise "Missing mandatory :server or :servers" unless server = params.delete(:server)
|
144
157
|
@servers = [ server ]
|
145
158
|
end
|
@@ -168,47 +181,27 @@ module ResilientSocket
|
|
168
181
|
# Timed out after 5 seconds trying to connect to the server
|
169
182
|
# Usually means server is busy or the remote server disappeared off the network recently
|
170
183
|
# No retry, just raise a ConnectionTimeout
|
184
|
+
#
|
185
|
+
# Note: When multiple servers are supplied it will only try to connect to
|
186
|
+
# the subsequent servers once the retry count has been exceeded
|
187
|
+
#
|
171
188
|
def connect
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
# TODO Implement failover to second server if connect fails
|
176
|
-
@server = @servers.first
|
177
|
-
|
178
|
-
host_name, port = @server.split(":")
|
179
|
-
port = port.to_i
|
180
|
-
address = Socket.getaddrinfo('localhost', nil, Socket::AF_INET)
|
181
|
-
|
182
|
-
socket = Socket.new(Socket.const_get(address[0][0]), Socket::SOCK_STREAM, 0)
|
183
|
-
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) unless @buffered
|
184
|
-
|
185
|
-
# http://stackoverflow.com/questions/231647/how-do-i-set-the-socket-timeout-in-ruby
|
189
|
+
if @servers.size > 0
|
190
|
+
# Try each server in sequence
|
191
|
+
@servers.each_with_index do |server, server_id|
|
186
192
|
begin
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
raise(ConnectionTimeout.new("Timedout after #{@connect_timeout} seconds trying to connect to #{host_name}:#{port}")) unless resp
|
192
|
-
begin
|
193
|
-
socket_address = Socket.pack_sockaddr_in(port, address[0][3])
|
194
|
-
socket.connect_nonblock(socket_address)
|
195
|
-
rescue Errno::EISCONN
|
196
|
-
end
|
197
|
-
end
|
198
|
-
@socket = socket
|
199
|
-
|
200
|
-
rescue SystemCallError => exception
|
201
|
-
if retries < @connect_retry_count
|
202
|
-
retries += 1
|
203
|
-
@logger.warn "Connection failure: #{exception.class}: #{exception.message}. Retry: #{retries}"
|
204
|
-
sleep @connect_retry_interval
|
205
|
-
retry
|
193
|
+
@socket = connect_to_server(server)
|
194
|
+
rescue ConnectionFailure => exc
|
195
|
+
# Raise Exception once it has also failed to connect to the last server
|
196
|
+
raise(exc) if @servers.size <= (server_id + 1)
|
206
197
|
end
|
207
|
-
@logger.error "Connection failure: #{exception.class}: #{exception.message}. Giving up after #{retries} retries"
|
208
|
-
raise ConnectionFailure.new("After #{retries} attempts: #{exception.class}: #{exception.message}")
|
209
198
|
end
|
199
|
+
else
|
200
|
+
@socket = connect_to_server(@servers.first)
|
210
201
|
end
|
211
|
-
|
202
|
+
|
203
|
+
# Invoke user supplied Block every time a new connection has been established
|
204
|
+
@on_connect.call(self) if @on_connect
|
212
205
|
true
|
213
206
|
end
|
214
207
|
|
@@ -232,54 +225,6 @@ module ResilientSocket
|
|
232
225
|
end
|
233
226
|
end
|
234
227
|
|
235
|
-
# Send data with retry logic
|
236
|
-
#
|
237
|
-
# On a connection failure, it will close the connection and retry the block
|
238
|
-
# Returns immediately on exception ReadTimeout
|
239
|
-
#
|
240
|
-
# Note this method should only wrap a single standalone call to the server
|
241
|
-
# since it will automatically retry the entire block every time a
|
242
|
-
# connection failure is experienced
|
243
|
-
#
|
244
|
-
# Error handling is implemented as follows:
|
245
|
-
# Network failure during send of either the header or the body
|
246
|
-
# Since the entire message was not sent it is assumed that it will not be processed
|
247
|
-
# Close socket
|
248
|
-
# Retry 1 time using a new connection before raising a ConnectionFailure
|
249
|
-
#
|
250
|
-
# Example of a resilient request that could _modify_ data at the server:
|
251
|
-
#
|
252
|
-
# # Only the send is within the retry block since we cannot re-send once
|
253
|
-
# # the send was successful
|
254
|
-
#
|
255
|
-
# Example of a resilient _read-only_ request:
|
256
|
-
#
|
257
|
-
# # Since the send can be sent many times it is safe to also put the receive
|
258
|
-
# # inside the retry block
|
259
|
-
#
|
260
|
-
def retry_on_connection_failure
|
261
|
-
retries = 0
|
262
|
-
begin
|
263
|
-
connect if closed?
|
264
|
-
yield(self)
|
265
|
-
rescue ConnectionFailure => exception
|
266
|
-
close
|
267
|
-
if retries < 3
|
268
|
-
retries += 1
|
269
|
-
@logger.warn "#retry_on_connection_failure Connection failure: #{exception.message}. Retry: #{retries}"
|
270
|
-
connect
|
271
|
-
retry
|
272
|
-
end
|
273
|
-
@logger.error "#retry_on_connection_failure Connection failure: #{exception.class}: #{exception.message}. Giving up after #{retries} retries"
|
274
|
-
raise ConnectionFailure.new("After #{retries} retry_on_connection_failure attempts: #{exception.class}: #{exception.message}")
|
275
|
-
rescue Exception => exc
|
276
|
-
# With any other exception we have to close the connection since the connection
|
277
|
-
# is now in an unknown state
|
278
|
-
close
|
279
|
-
raise exc
|
280
|
-
end
|
281
|
-
end
|
282
|
-
|
283
228
|
# 4. TCP receive timeout:
|
284
229
|
# Send was successful but receive timed out after X seconds (for example 10 seconds)
|
285
230
|
# No data or partial data received ( for example header but no body )
|
@@ -327,6 +272,88 @@ module ResilientSocket
|
|
327
272
|
buffer
|
328
273
|
end
|
329
274
|
|
275
|
+
# Send and/or receive data with automatic retry on connection failure
|
276
|
+
#
|
277
|
+
# On a connection failure, it will close the connection and retry the block
|
278
|
+
# Returns immediately on exception ReadTimeout
|
279
|
+
#
|
280
|
+
# 1. Example of a resilient _readonly_ request:
|
281
|
+
#
|
282
|
+
# When reading data from a server that does not change state on the server
|
283
|
+
# Wrap both the send and the read with #retry_on_connection_failure
|
284
|
+
# since it is safe to send the same data twice to the server
|
285
|
+
#
|
286
|
+
# # Since the send can be sent many times it is safe to also put the receive
|
287
|
+
# # inside the retry block
|
288
|
+
# value = client.retry_on_connection_failure do
|
289
|
+
# client.send("GETVALUE:count\n")
|
290
|
+
# client.read(20).strip.to_i
|
291
|
+
# end
|
292
|
+
#
|
293
|
+
# 2. Example of a resilient request that _modifies_ data on the server:
|
294
|
+
#
|
295
|
+
# When changing state on the server, for example when updating a value
|
296
|
+
# Wrap _only_ the send with #retry_on_connection_failure
|
297
|
+
# The read must be outside the #retry_on_connection_failure since we must
|
298
|
+
# not retry the send if the connection fails during the #read
|
299
|
+
#
|
300
|
+
# value = 45
|
301
|
+
# # Only the send is within the retry block since we cannot re-send once
|
302
|
+
# # the send was successful since the server may have made the change
|
303
|
+
# client.retry_on_connection_failure do
|
304
|
+
# client.send("SETVALUE:#{count}\n")
|
305
|
+
# end
|
306
|
+
# # Server returns "SAVED" if the call was successfull
|
307
|
+
# result = client.read(20).strip
|
308
|
+
#
|
309
|
+
# 3. Example of a resilient request that _modifies_ data on the server:
|
310
|
+
#
|
311
|
+
# When changing state on the server, for example when updating a value
|
312
|
+
# Wrap _only_ the send with #retry_on_connection_failure
|
313
|
+
# The read must be outside the #retry_on_connection_failure since we must
|
314
|
+
# not retry the send if the connection fails during the #read
|
315
|
+
#
|
316
|
+
# value = 45
|
317
|
+
# # Only the send is within the retry block since we cannot re-send once
|
318
|
+
# # the send was successful since the server may have made the change
|
319
|
+
# client.retry_on_connection_failure do
|
320
|
+
# client.send("SETVALUE:#{count}\n")
|
321
|
+
# end
|
322
|
+
# # Server returns "SAVED" if the call was successfull
|
323
|
+
# saved = (client.read(20).strip == 'SAVED')
|
324
|
+
#
|
325
|
+
#
|
326
|
+
# Error handling is implemented as follows:
|
327
|
+
# If a network failure occurrs during the block invocation the block
|
328
|
+
# will be called again with a new connection to the server.
|
329
|
+
# It will only be retried up to 3 times
|
330
|
+
# The re-connect will independently retry and timeout using all the
|
331
|
+
# rules of #connect
|
332
|
+
#
|
333
|
+
#
|
334
|
+
def retry_on_connection_failure
|
335
|
+
retries = 0
|
336
|
+
begin
|
337
|
+
connect if closed?
|
338
|
+
yield(self)
|
339
|
+
rescue ConnectionFailure => exception
|
340
|
+
close
|
341
|
+
if retries < 3
|
342
|
+
retries += 1
|
343
|
+
@logger.warn "#retry_on_connection_failure Connection failure: #{exception.message}. Retry: #{retries}"
|
344
|
+
connect
|
345
|
+
retry
|
346
|
+
end
|
347
|
+
@logger.error "#retry_on_connection_failure Connection failure: #{exception.class}: #{exception.message}. Giving up after #{retries} retries"
|
348
|
+
raise ConnectionFailure.new("After #{retries} retry_on_connection_failure attempts: #{exception.class}: #{exception.message}")
|
349
|
+
rescue Exception => exc
|
350
|
+
# With any other exception we have to close the connection since the connection
|
351
|
+
# is now in an unknown state
|
352
|
+
close
|
353
|
+
raise exc
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
330
357
|
# Close the socket
|
331
358
|
#
|
332
359
|
# Logs a warning if an error occurs trying to close the socket
|
@@ -346,6 +373,54 @@ module ResilientSocket
|
|
346
373
|
@socket.setsockopt(level, optname, optval)
|
347
374
|
end
|
348
375
|
|
349
|
-
|
376
|
+
#############################################
|
377
|
+
protected
|
378
|
+
|
379
|
+
# Try connecting to a single server
|
380
|
+
# Returns the connected socket
|
381
|
+
#
|
382
|
+
# Raises ConnectionTimeout when the connection timeout has been exceeded
|
383
|
+
# Raises ConnectionFailure
|
384
|
+
def connect_to_server(server)
|
385
|
+
socket = nil
|
386
|
+
retries = 0
|
387
|
+
@logger.benchmark_info "Connecting to server #{server}" do
|
388
|
+
begin
|
389
|
+
host_name, port = server.split(":")
|
390
|
+
port = port.to_i
|
391
|
+
address = Socket.getaddrinfo('localhost', nil, Socket::AF_INET)
|
392
|
+
|
393
|
+
socket = Socket.new(Socket.const_get(address[0][0]), Socket::SOCK_STREAM, 0)
|
394
|
+
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) unless buffered
|
395
|
+
|
396
|
+
# http://stackoverflow.com/questions/231647/how-do-i-set-the-socket-timeout-in-ruby
|
397
|
+
begin
|
398
|
+
socket_address = Socket.pack_sockaddr_in(port, address[0][3])
|
399
|
+
socket.connect_nonblock(socket_address)
|
400
|
+
rescue Errno::EINPROGRESS
|
401
|
+
resp = IO.select(nil, [socket], nil, @connect_timeout)
|
402
|
+
raise(ConnectionTimeout.new("Timedout after #{@connect_timeout} seconds trying to connect to #{host_name}:#{port}")) unless resp
|
403
|
+
begin
|
404
|
+
socket_address = Socket.pack_sockaddr_in(port, address[0][3])
|
405
|
+
socket.connect_nonblock(socket_address)
|
406
|
+
rescue Errno::EISCONN
|
407
|
+
end
|
408
|
+
end
|
409
|
+
break
|
410
|
+
rescue SystemCallError => exception
|
411
|
+
if retries < @connect_retry_count
|
412
|
+
retries += 1
|
413
|
+
@logger.warn "Connection failure: #{exception.class}: #{exception.message}. Retry: #{retries}"
|
414
|
+
sleep @connect_retry_interval
|
415
|
+
retry
|
416
|
+
end
|
417
|
+
@logger.error "Connection failure: #{exception.class}: #{exception.message}. Giving up after #{retries} retries"
|
418
|
+
raise ConnectionFailure.new("After #{retries} attempts: #{exception.class}: #{exception.message}")
|
419
|
+
end
|
420
|
+
end
|
421
|
+
@server = server
|
422
|
+
socket
|
423
|
+
end
|
350
424
|
|
425
|
+
end
|
351
426
|
end
|
File without changes
|
Binary file
|
data/test/tcp_client_test.rb
CHANGED
@@ -95,6 +95,44 @@ class TCPClientTest < Test::Unit::TestCase
|
|
95
95
|
end
|
96
96
|
|
97
97
|
end
|
98
|
+
|
99
|
+
context "without client connection" do
|
100
|
+
should "connect to second server when first is down" do
|
101
|
+
client = ResilientSocket::TCPClient.new(
|
102
|
+
:servers => ['localhost:1999', @server_name],
|
103
|
+
:read_timeout => 3
|
104
|
+
)
|
105
|
+
assert_equal @server_name, client.server
|
106
|
+
|
107
|
+
request = { 'action' => 'test1' }
|
108
|
+
client.send(BSON.serialize(request))
|
109
|
+
reply = read_bson_document(client)
|
110
|
+
assert_equal 'test1', reply['result']
|
111
|
+
|
112
|
+
client.close
|
113
|
+
end
|
114
|
+
|
115
|
+
should "call on_connect after connection" do
|
116
|
+
client = ResilientSocket::TCPClient.new(
|
117
|
+
:server => @server_name,
|
118
|
+
:read_timeout => 3,
|
119
|
+
:on_connect => Proc.new do |socket|
|
120
|
+
# Reset user_data on each connection
|
121
|
+
socket.user_data = { :sequence => 1 }
|
122
|
+
end
|
123
|
+
)
|
124
|
+
assert_equal @server_name, client.server
|
125
|
+
assert_equal 1, client.user_data[:sequence]
|
126
|
+
|
127
|
+
request = { 'action' => 'test1' }
|
128
|
+
client.send(BSON.serialize(request))
|
129
|
+
reply = read_bson_document(client)
|
130
|
+
assert_equal 'test1', reply['result']
|
131
|
+
|
132
|
+
client.close
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
98
136
|
end
|
99
137
|
|
100
138
|
end
|
data/test.log
CHANGED
Binary file
|
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.0
|
4
|
+
version: 0.1.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-04 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: semantic_logger
|
@@ -39,6 +39,7 @@ files:
|
|
39
39
|
- lib/resilient_socket/version.rb
|
40
40
|
- lib/resilient_socket.rb
|
41
41
|
- LICENSE.txt
|
42
|
+
- nbproject/private/config.properties
|
42
43
|
- nbproject/private/private.properties
|
43
44
|
- nbproject/private/rake-d.txt
|
44
45
|
- nbproject/project.properties
|
@@ -46,6 +47,7 @@ files:
|
|
46
47
|
- Rakefile
|
47
48
|
- README.md
|
48
49
|
- resilient_socket-0.0.1.gem
|
50
|
+
- resilient_socket-0.0.2.gem
|
49
51
|
- test/simple_tcp_server.rb
|
50
52
|
- test/tcp_client_test.rb
|
51
53
|
- test.log
|