resilient_socket 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 currently connected to or being connected to
39
- # including the port number
38
+ # Returns [String] Name of the server connected to including the port number
40
39
  #
41
40
  # Example:
42
- # "localhost:2000"
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[:servers]
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
- retries = 0
173
- @logger.benchmark_info "Connected to server" do
174
- begin
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
- socket_address = Socket.pack_sockaddr_in(port, address[0][3])
188
- socket.connect_nonblock(socket_address)
189
- rescue Errno::EINPROGRESS
190
- resp = IO.select(nil, [socket], nil, @connect_timeout)
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
- self.user_data = nil
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
- end
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
@@ -1,3 +1,3 @@
1
1
  module ResilientSocket #:nodoc
2
- VERSION = "0.0.2"
2
+ VERSION = "0.1.0"
3
3
  end
File without changes
@@ -1,2 +1,3 @@
1
1
  file.reference.resilient_socket-lib=/Users/rmorrison/Sandbox/resilient_socket/lib
2
2
  file.reference.resilient_socket-test=/Users/rmorrison/Sandbox/resilient_socket/test
3
+ platform.active=Ruby_0
@@ -1,5 +1,6 @@
1
1
  file.reference.resilient_socket-lib=lib
2
2
  file.reference.resilient_socket-test=test
3
+ javac.classpath=
3
4
  main.file=
4
5
  platform.active=Ruby
5
6
  source.encoding=UTF-8
Binary file
@@ -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.2
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-01 00:00:00.000000000 Z
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