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