net_tcp_client 1.0.2 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,36 @@
1
+ module Net
2
+ class TCPClient
3
+ module Policy
4
+ # Policy for connecting to servers in the order specified
5
+ class Base
6
+ attr_reader :addresses
7
+
8
+ # Returns a policy instance for the supplied policy type
9
+ def self.factory(policy, server_names)
10
+ case policy
11
+ when :ordered
12
+ # Policy for connecting to servers in the order specified
13
+ Ordered.new(server_names)
14
+ when :random
15
+ Random.new(server_names)
16
+ when Proc
17
+ Custom.new(server_names, policy)
18
+ else
19
+ raise(ArgumentError, "Invalid policy: #{policy.inspect}")
20
+ end
21
+ end
22
+
23
+ def initialize(server_names)
24
+ # Collect Addresses for the supplied server_names
25
+ @addresses = Array(server_names).collect { |name| Address.addresses_for_server_name(name) }.flatten
26
+ end
27
+
28
+ # Calls the block once for each server, with the addresses in order
29
+ def each(&block)
30
+ raise NotImplementedError
31
+ end
32
+
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,39 @@
1
+ module Net
2
+ class TCPClient
3
+ module Policy
4
+ # Policy for connecting to servers in the order specified
5
+ class Custom < Base
6
+ def initialize(server_names, proc)
7
+ super(server_names)
8
+ @proc = proc
9
+ end
10
+
11
+ # Calls the block once for each server, with the addresses in the order returned
12
+ # by the supplied proc.
13
+ # The block must return a Net::TCPClient::Address instance,
14
+ # or nil to stop trying to connect to servers
15
+ #
16
+ # Note:
17
+ # If every address fails the block will be called constantly until it returns nil.
18
+ #
19
+ # Example:
20
+ # # Returns addresses in random order but without checking if a host name has been used before
21
+ # policy.each_proc do |addresses, count|
22
+ # # Return nil after the last address has been tried so that retry logic can take over
23
+ # if count <= address.size
24
+ # addresses.sample
25
+ # end
26
+ # end
27
+ def each(&block)
28
+ count = 1
29
+ while address = @proc.call(addresses, count)
30
+ raise(ArgumentError, 'Proc must return Net::TCPClient::Address, or nil') unless address.is_a?(Net::TCPClient::Address) || address.nil?
31
+ block.call(address)
32
+ count += 1
33
+ end
34
+ end
35
+
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,14 @@
1
+ module Net
2
+ class TCPClient
3
+ module Policy
4
+ # Policy for connecting to servers in the order specified
5
+ class Ordered < Base
6
+ # Calls the block once for each server, with the addresses in order
7
+ def each(&block)
8
+ addresses.each {|address| block.call(address)}
9
+ end
10
+
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ module Net
2
+ class TCPClient
3
+ module Policy
4
+ # Policy for connecting to servers in the order specified
5
+ class Random < Base
6
+ # Calls the block once for each server, with the addresses in random order
7
+ def each(&block)
8
+ addresses.shuffle.each {|address| block.call(address)}
9
+ end
10
+
11
+ end
12
+ end
13
+ end
14
+ end
@@ -35,25 +35,19 @@ module Net
35
35
  # has to be completely destroyed and recreated after a connection failure
36
36
  #
37
37
  class TCPClient
38
+ include SemanticLogger::Loggable if defined?(SemanticLogger::Loggable)
39
+
40
+ attr_accessor :connect_timeout, :read_timeout, :write_timeout,
41
+ :connect_retry_count, :connect_retry_interval, :retry_count,
42
+ :policy, :close_on_error, :buffered, :ssl, :buffered,
43
+ :proxy_server
44
+ attr_reader :servers, :address, :socket, :ssl_handshake_timeout
45
+
38
46
  # Supports embedding user supplied data along with this connection
39
47
  # such as sequence number and other connection specific information
48
+ # Not used or modified by TCPClient
40
49
  attr_accessor :user_data
41
50
 
42
- # Returns [String] Name of the server connected to including the port number
43
- #
44
- # Example:
45
- # localhost:2000
46
- attr_reader :server
47
-
48
- attr_accessor :read_timeout, :connect_timeout, :connect_retry_count,
49
- :retry_count, :connect_retry_interval, :server_selector, :close_on_error
50
-
51
- # Returns [true|false] Whether send buffering is enabled for this connection
52
- attr_reader :buffered
53
-
54
- # Returns the logger being used by the TCPClient instance
55
- attr_reader :logger
56
-
57
51
  @@reconnect_on_errors = [
58
52
  Errno::ECONNABORTED,
59
53
  Errno::ECONNREFUSED,
@@ -65,6 +59,7 @@ module Net
65
59
  Errno::EPIPE,
66
60
  Errno::ETIMEDOUT,
67
61
  EOFError,
62
+ Net::TCPClient::ConnectionTimeout
68
63
  ]
69
64
 
70
65
  # Return the array of errors that will result in an automatic connection retry
@@ -107,6 +102,7 @@ module Net
107
102
  # :server [String]
108
103
  # URL of the server to connect to with port number
109
104
  # 'localhost:2000'
105
+ # '192.168.1.10:80'
110
106
  #
111
107
  # :servers [Array of String]
112
108
  # Array of URL's of servers to connect to with port numbers
@@ -117,34 +113,29 @@ module Net
117
113
  # A read failure or timeout will not result in switching to the second
118
114
  # server, only a connection failure or during an automatic reconnect
119
115
  #
120
- # :read_timeout [Float]
121
- # Time in seconds to timeout on read
122
- # Can be overridden by supplying a timeout in the read call
123
- # Default: 60
124
- #
125
116
  # :connect_timeout [Float]
126
117
  # Time in seconds to timeout when trying to connect to the server
127
118
  # A value of -1 will cause the connect wait time to be infinite
128
- # Default: Half of the :read_timeout ( 30 seconds )
119
+ # Default: 10 seconds
129
120
  #
130
- # :logger [Logger]
131
- # Set the logger to which to write log messages to
132
- # Note: Additional methods will be mixed into this logger to make it
133
- # compatible with the SematicLogger extensions if it is not already
134
- # a SemanticLogger logger instance
121
+ # :read_timeout [Float]
122
+ # Time in seconds to timeout on read
123
+ # Can be overridden by supplying a timeout in the read call
124
+ # Default: 60
135
125
  #
136
- # :log_level [Symbol]
137
- # Set the logging level for the TCPClient
138
- # Any valid SemanticLogger log level:
139
- # :trace, :debug, :info, :warn, :error, :fatal
140
- # Default: SemanticLogger.default_level
126
+ # :write_timeout [Float]
127
+ # Time in seconds to timeout on write
128
+ # Can be overridden by supplying a timeout in the write call
129
+ # Default: 60
141
130
  #
142
131
  # :buffered [Boolean]
143
132
  # Whether to use Nagle's Buffering algorithm (http://en.wikipedia.org/wiki/Nagle's_algorithm)
144
133
  # Recommend disabling for RPC style invocations where we don't want to wait for an
145
134
  # ACK from the server before sending the last partial segment
146
135
  # Buffering is recommended in a browser or file transfer style environment
147
- # where multiple sends are expected during a single response
136
+ # where multiple sends are expected during a single response.
137
+ # Also sets sync to true if buffered is false so that all data is sent immediately without
138
+ # internal buffering.
148
139
  # Default: true
149
140
  #
150
141
  # :connect_retry_count [Fixnum]
@@ -159,20 +150,19 @@ module Net
159
150
  # Number of times to retry when calling #retry_on_connection_failure
160
151
  # This is independent of :connect_retry_count which still applies with
161
152
  # connection failures. This retry controls upto how many times to retry the
162
- # supplied block should a connection failure occurr during the block
153
+ # supplied block should a connection failure occur during the block
163
154
  # Default: 3
164
155
  #
165
156
  # :on_connect [Proc]
166
157
  # Directly after a connection is established and before it is made available
167
158
  # for use this Block is invoked.
168
159
  # Typical Use Cases:
169
- # - Initialize per connection session sequence numbers
170
- # - Pass any authentication information to the server
171
- # - Perform a handshake with the server
160
+ # - Initialize per connection session sequence numbers.
161
+ # - Pass authentication information to the server.
162
+ # - Perform a handshake with the server.
172
163
  #
173
- # :server_selector [Symbol|Proc]
174
- # When multiple servers are supplied using :servers, this option will
175
- # determine which server is selected from the list
164
+ # :policy [Symbol|Proc]
165
+ # Specify the policy to use when connecting to servers.
176
166
  # :ordered
177
167
  # Select a server in the order supplied in the array, with the first
178
168
  # having the highest priority. The second server will only be connected
@@ -180,20 +170,14 @@ module Net
180
170
  # :random
181
171
  # Randomly select a server from the list every time a connection
182
172
  # is established, including during automatic connection recovery.
183
- # :nearest
184
- # FUTURE - Not implemented yet
185
- # The server with an IP address that most closely matches the
186
- # local ip address will be attempted first
187
- # This will result in connections to servers on the localhost
188
- # first prior to looking at remote servers
189
173
  # :ping_time
190
- # FUTURE - Not implemented yet
191
- # The server with the lowest ping time will be selected first
174
+ # FUTURE - Not implemented yet - Pull request anyone?
175
+ # The server with the lowest ping time will be tried first
192
176
  # Proc:
193
177
  # When a Proc is supplied, it will be called passing in the list
194
178
  # of servers. The Proc must return one server name
195
179
  # Example:
196
- # :server_selector => Proc.new do |servers|
180
+ # :policy => Proc.new do |servers|
197
181
  # servers.last
198
182
  # end
199
183
  # Default: :ordered
@@ -204,7 +188,26 @@ module Net
204
188
  # This includes a Read Timeout
205
189
  # Default: true
206
190
  #
207
- # Example
191
+ # :proxy_server [String]
192
+ # The host name and port in the form of 'host_name:1234' to forward
193
+ # socket connections though.
194
+ # Default: nil ( none )
195
+ #
196
+ # SSL Options
197
+ # :ssl [true|false|Hash]
198
+ # true: SSL is enabled using the SSL context defaults.
199
+ # false: SSL is not used.
200
+ # Hash:
201
+ # Keys from OpenSSL::SSL::SSLContext:
202
+ # ca_file, ca_path, cert, cert_store, ciphers, key, ssl_timeout, ssl_version
203
+ # verify_callback, verify_depth, verify_mode
204
+ # handshake_timeout: [Float]
205
+ # The number of seconds to timeout the SSL Handshake.
206
+ # Default: connect_timeout
207
+ # Default: false.
208
+ # See OpenSSL::SSL::SSLContext::DEFAULT_PARAMS for the defaults.
209
+ #
210
+ # Example:
208
211
  # client = Net::TCPClient.new(
209
212
  # server: 'server:3300',
210
213
  # connect_retry_interval: 0.1,
@@ -220,20 +223,43 @@ module Net
220
223
  #
221
224
  # puts "Received: #{response}"
222
225
  # client.close
226
+ #
227
+ # SSL Example:
228
+ # client = Net::TCPClient.new(
229
+ # server: 'server:3300',
230
+ # connect_retry_interval: 0.1,
231
+ # connect_retry_count: 5,
232
+ # ssl: true
233
+ # )
234
+ #
235
+ # SSL with options Example:
236
+ # client = Net::TCPClient.new(
237
+ # server: 'server:3300',
238
+ # connect_retry_interval: 0.1,
239
+ # connect_retry_count: 5,
240
+ # ssl: {
241
+ # verify_mode: OpenSSL::SSL::VERIFY_NONE
242
+ # }
243
+ # )
223
244
  def initialize(parameters={})
224
245
  params = parameters.dup
225
246
  @read_timeout = (params.delete(:read_timeout) || 60.0).to_f
226
- @connect_timeout = (params.delete(:connect_timeout) || (@read_timeout/2)).to_f
247
+ @write_timeout = (params.delete(:write_timeout) || 60.0).to_f
248
+ @connect_timeout = (params.delete(:connect_timeout) || 10).to_f
227
249
  buffered = params.delete(:buffered)
228
250
  @buffered = buffered.nil? ? true : buffered
229
251
  @connect_retry_count = params.delete(:connect_retry_count) || 10
230
252
  @retry_count = params.delete(:retry_count) || 3
231
253
  @connect_retry_interval = (params.delete(:connect_retry_interval) || 0.5).to_f
232
254
  @on_connect = params.delete(:on_connect)
233
- @server_selector = params.delete(:server_selector) || :ordered
255
+ @proxy_server = params.delete(:proxy_server)
256
+ @policy = params.delete(:policy) || :ordered
234
257
  @close_on_error = params.delete(:close_on_error)
235
258
  @close_on_error = true if @close_on_error.nil?
236
- @logger = params.delete(:logger)
259
+ if @ssl = params.delete(:ssl)
260
+ @ssl = {} if @ssl == true
261
+ @ssl_handshake_timeout = (@ssl.delete(:handshake_timeout) || @connect_timeout).to_f
262
+ end
237
263
 
238
264
  if server = params.delete(:server)
239
265
  @servers = [server]
@@ -243,9 +269,9 @@ module Net
243
269
  end
244
270
  raise(ArgumentError, 'Missing mandatory :server or :servers') unless @servers
245
271
 
246
- # If a logger is supplied then extend it with the SemanticLogger API
247
- @logger = Logging.new_logger(logger, "#{self.class.name} #{@servers.inspect}", params.delete(:log_level))
248
-
272
+ if params.delete(:logger)
273
+ warn '[Deprecated] :logger option is no longer offered. Add semantic_logger gem to enable logging.' if $VERBOSE
274
+ end
249
275
  raise(ArgumentError, "Invalid options: #{params.inspect}") if params.size > 0
250
276
 
251
277
  # Connect to the Server
@@ -277,23 +303,28 @@ module Net
277
303
  # Note: Calling #connect on an open connection will close the current connection
278
304
  # and create a new connection
279
305
  def connect
280
- @socket.close if @socket && !@socket.closed?
281
- case
282
- when @servers.size == 1
283
- connect_to_server(@servers.first)
284
- when @server_selector.is_a?(Proc)
285
- connect_to_server(@server_selector.call(@servers))
286
- when @server_selector == :ordered
287
- connect_to_servers_in_order(@servers)
288
- when @server_selector == :random
289
- connect_to_servers_in_order(@servers.sample(@servers.size))
290
- else
291
- raise ArgumentError.new("Invalid or unknown value for parameter :server_selector => #{@server_selector}")
292
- end
306
+ start_time = Time.now
307
+ retries = 0
308
+ close
293
309
 
294
- # Invoke user supplied Block every time a new connection has been established
295
- @on_connect.call(self) if @on_connect
296
- true
310
+ # Number of times to try
311
+ begin
312
+ connect_to_server(servers, policy)
313
+ logger.info(message: "Connected to #{address}", duration: (Time.now - start_time) * 1000) if respond_to?(:logger)
314
+ rescue ConnectionFailure, ConnectionTimeout => exception
315
+ cause = exception.is_a?(ConnectionTimeout) ? exception : exception.cause
316
+ # Retry-able?
317
+ if self.class.reconnect_on_errors.include?(cause.class) && (retries < connect_retry_count.to_i)
318
+ retries += 1
319
+ logger.warn "#connect Failed to connect to any of #{servers.join(',')}. Sleeping:#{connect_retry_interval}s. Retry: #{retries}" if respond_to?(:logger)
320
+ sleep(connect_retry_interval)
321
+ retry
322
+ else
323
+ message = "#connect Failed to connect to any of #{servers.join(',')} after #{retries} retries. #{exception.class}: #{exception.message}"
324
+ logger.benchmark_error(message, exception: exception, duration: (Time.now - start_time)) if respond_to?(:logger)
325
+ raise ConnectionFailure.new(message, address.to_s, cause)
326
+ end
327
+ end
297
328
  end
298
329
 
299
330
  # Send data to the server
@@ -303,24 +334,35 @@ module Net
303
334
  # Raises Net::TCPClient::ConnectionFailure whenever the send fails
304
335
  # For a description of the errors, see Socket#write
305
336
  #
306
- def write(data)
337
+ # Parameters
338
+ # timeout [Float]
339
+ # Optional: Override the default write timeout for this write
340
+ # Number of seconds before raising Net::TCPClient::WriteTimeout when no data has
341
+ # been written.
342
+ # A value of -1 will wait forever
343
+ # Default: :write_timeout supplied to #initialize
344
+ #
345
+ # Note: After a Net::TCPClient::ReadTimeout #read can be called again on
346
+ # the same socket to read the response later.
347
+ # If the application no longers want the connection after a
348
+ # Net::TCPClient::ReadTimeout, then the #close method _must_ be called
349
+ # before calling _connect_ or _retry_on_connection_failure_ to create
350
+ # a new connection
351
+ def write(data, timeout = write_timeout)
307
352
  data = data.to_s
308
- logger.trace('#write ==> sending', data)
309
- stats = {}
310
- logger.benchmark_debug('#write ==> complete', stats) do
311
- begin
312
- stats[:bytes_sent] = @socket.write(data)
313
- rescue SystemCallError => exception
314
- logger.warn "#write Connection failure: #{exception.class}: #{exception.message}"
315
- close if close_on_error
316
- raise Net::TCPClient::ConnectionFailure.new("Send Connection failure: #{exception.class}: #{exception.message}", @server, exception)
317
- rescue Exception
318
- # Close the connection on any other exception since the connection
319
- # will now be in an inconsistent state
320
- close if close_on_error
321
- raise
353
+ if respond_to?(:logger)
354
+ payload = {timeout: timeout}
355
+ # With trace level also log the sent data
356
+ payload[:data] = data if logger.trace?
357
+ logger.benchmark_debug('#write', payload: payload) do
358
+ payload[:bytes] = socket_write(data, timeout)
322
359
  end
360
+ else
361
+ socket_write(data, timeout)
323
362
  end
363
+ rescue Exception => exc
364
+ close if close_on_error
365
+ raise exc
324
366
  end
325
367
 
326
368
  # Returns a response from the server
@@ -340,9 +382,12 @@ module Net
340
382
  # Parameters
341
383
  # length [Fixnum]
342
384
  # The number of bytes to return
343
- # #read will not return unitl 'length' bytes have been received from
385
+ # #read will not return until 'length' bytes have been received from
344
386
  # the server
345
387
  #
388
+ # buffer [String]
389
+ # Optional buffer into which to write the data that is read.
390
+ #
346
391
  # timeout [Float]
347
392
  # Optional: Override the default read timeout for this read
348
393
  # Number of seconds before raising Net::TCPClient::ReadTimeout when no data has
@@ -350,41 +395,27 @@ module Net
350
395
  # A value of -1 will wait forever for a response on the socket
351
396
  # Default: :read_timeout supplied to #initialize
352
397
  #
353
- # Note: After a ResilientSocket::Net::TCPClient::ReadTimeout #read can be called again on
398
+ # Note: After a Net::TCPClient::ReadTimeout #read can be called again on
354
399
  # the same socket to read the response later.
355
400
  # If the application no longers want the connection after a
356
401
  # Net::TCPClient::ReadTimeout, then the #close method _must_ be called
357
402
  # before calling _connect_ or _retry_on_connection_failure_ to create
358
403
  # a new connection
359
- #
360
404
  def read(length, buffer = nil, timeout = read_timeout)
361
- result = nil
362
- logger.benchmark_debug("#read <== read #{length} bytes") do
363
- wait_for_data(timeout)
364
-
365
- # Read data from socket
366
- begin
367
- result = buffer.nil? ? @socket.read(length) : @socket.read(length, buffer)
368
- logger.trace('#read <== received', result)
369
-
370
- # EOF before all the data was returned
371
- if result.nil? || (result.length < length)
372
- close if close_on_error
373
- logger.warn "#read server closed the connection before #{length} bytes were returned"
374
- raise Net::TCPClient::ConnectionFailure.new('Connection lost while reading data', @server, EOFError.new('end of file reached'))
375
- end
376
- rescue SystemCallError, IOError => exception
377
- close if close_on_error
378
- logger.warn "#read Connection failure while reading data: #{exception.class}: #{exception.message}"
379
- raise Net::TCPClient::ConnectionFailure.new("#{exception.class}: #{exception.message}", @server, exception)
380
- rescue Exception
381
- # Close the connection on any other exception since the connection
382
- # will now be in an inconsistent state
383
- close if close_on_error
384
- raise
405
+ if respond_to?(:logger)
406
+ payload = {bytes: length, timeout: timeout}
407
+ logger.benchmark_debug('#read', payload: payload) do
408
+ data = socket_read(length, buffer, timeout)
409
+ # With trace level also log the received data
410
+ payload[:data] = data if logger.trace?
411
+ data
385
412
  end
413
+ else
414
+ socket_read(length, buffer, timeout)
386
415
  end
387
- result
416
+ rescue Exception => exc
417
+ close if close_on_error
418
+ raise exc
388
419
  end
389
420
 
390
421
  # Send and/or receive data with automatic retry on connection failure
@@ -433,20 +464,20 @@ module Net
433
464
  begin
434
465
  connect if closed?
435
466
  yield(self)
436
- rescue Net::TCPClient::ConnectionFailure => exception
467
+ rescue ConnectionFailure => exception
437
468
  exc_str = exception.cause ? "#{exception.cause.class}: #{exception.cause.message}" : exception.message
438
469
  # Re-raise exceptions that should not be retried
439
470
  if !self.class.reconnect_on_errors.include?(exception.cause.class)
440
- logger.warn "#retry_on_connection_failure not configured to retry: #{exc_str}"
471
+ logger.info "#retry_on_connection_failure not configured to retry: #{exc_str}" if respond_to?(:logger)
441
472
  raise exception
442
473
  elsif retries < @retry_count
443
474
  retries += 1
444
- logger.warn "#retry_on_connection_failure retry #{retries} due to #{exception.class}: #{exception.message}"
475
+ logger.warn "#retry_on_connection_failure retry #{retries} due to #{exception.class}: #{exception.message}" if respond_to?(:logger)
445
476
  connect
446
477
  retry
447
478
  end
448
- logger.error "#retry_on_connection_failure Connection failure: #{exception.class}: #{exception.message}. Giving up after #{retries} retries"
449
- raise Net::TCPClient::ConnectionFailure.new("After #{retries} retries to host '#{server}': #{exc_str}", @server, exception.cause)
479
+ logger.error "#retry_on_connection_failure Connection failure: #{exception.class}: #{exception.message}. Giving up after #{retries} retries" if respond_to?(:logger)
480
+ raise ConnectionFailure.new("After #{retries} retries to host '#{server}': #{exc_str}", server, exception.cause)
450
481
  end
451
482
  end
452
483
 
@@ -454,14 +485,26 @@ module Net
454
485
  #
455
486
  # Logs a warning if an error occurs trying to close the socket
456
487
  def close
457
- @socket.close unless @socket.closed?
488
+ socket.close if socket && !socket.closed?
489
+ @socket = nil
490
+ @address = nil
491
+ true
458
492
  rescue IOError => exception
459
- logger.warn "IOError when attempting to close socket: #{exception.class}: #{exception.message}"
493
+ logger.warn "IOError when attempting to close socket: #{exception.class}: #{exception.message}" if respond_to?(:logger)
494
+ false
495
+ end
496
+
497
+ def flush
498
+ return unless socket
499
+ respond_to?(:logger) ? logger.benchmark_debug('#flush') { socket.flush } : socket.flush
460
500
  end
461
501
 
462
- # Returns whether the socket is closed
463
502
  def closed?
464
- @socket.closed?
503
+ socket.nil? || socket.closed?
504
+ end
505
+
506
+ def eof?
507
+ socket.nil? || socket.eof?
465
508
  end
466
509
 
467
510
  # Returns whether the connection to the server is alive
@@ -478,10 +521,10 @@ module Net
478
521
  # make about 120,000 calls per second against an active connection.
479
522
  # I.e. About 8.3 micro seconds per call
480
523
  def alive?
481
- return false if @socket.closed?
524
+ return false if socket.nil? || closed?
482
525
 
483
- if IO.select([@socket], nil, nil, 0)
484
- !@socket.eof? rescue false
526
+ if IO.select([socket], nil, nil, 0)
527
+ !socket.eof? rescue false
485
528
  else
486
529
  true
487
530
  end
@@ -489,109 +532,189 @@ module Net
489
532
  false
490
533
  end
491
534
 
492
- # See: Socket#setsockopt
493
- def setsockopt(level, optname, optval)
494
- @socket.setsockopt(level, optname, optval)
535
+ def setsockopt(*args)
536
+ socket.nil? || socket.setsockopt(*args)
495
537
  end
496
538
 
497
- #############################################
498
- protected
539
+ private
499
540
 
500
- # Try connecting to a single server
501
- # Returns the connected socket
541
+ # Connect to one of the servers in the list, per the current policy
542
+ # Returns [Socket] the socket connected to or an Exception
543
+ def connect_to_server(servers, policy)
544
+ # Iterate over each server address until it successfully connects to a host
545
+ last_exception = nil
546
+ Policy::Base.factory(policy, servers).each do |address|
547
+ begin
548
+ return connect_to_address(address)
549
+ rescue ConnectionTimeout, ConnectionFailure => exception
550
+ last_exception = exception
551
+ end
552
+ end
553
+
554
+ # Raise Exception once it has failed to connect to any server
555
+ last_exception ? raise(last_exception) : raise(ArgumentError, "No servers supplied to connect to: #{servers.join(',')}")
556
+ end
557
+
558
+ # Returns [Socket] connected to supplied address
559
+ # address [Net::TCPClient::Address]
560
+ # Host name, ip address and port of server to connect to
561
+ # Connect to the server at the supplied address
562
+ # Returns the socket connection
563
+ def connect_to_address(address)
564
+ socket =
565
+ if proxy_server
566
+ ::SOCKSSocket.new("#{address.ip_address}:#{address.port}", proxy_server)
567
+ else
568
+ ::Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, 0)
569
+ end
570
+ unless buffered
571
+ socket.sync = true
572
+ socket.setsockopt(IPPROTO_TCP, TCP_NODELAY, 1)
573
+ end
574
+
575
+ socket_connect(socket, address, connect_timeout)
576
+
577
+ @socket = ssl ? ssl_connect(socket, address, ssl_handshake_timeout) : socket
578
+ @address = address
579
+
580
+ # Invoke user supplied Block every time a new connection has been established
581
+ @on_connect.call(self) if @on_connect
582
+ end
583
+
584
+ # Connect to server
502
585
  #
503
586
  # Raises Net::TCPClient::ConnectionTimeout when the connection timeout has been exceeded
504
587
  # Raises Net::TCPClient::ConnectionFailure
505
- def connect_to_server(server)
506
- # Have to use Socket internally instead of TCPSocket since TCPSocket
507
- # does not offer async connect API amongst others:
508
- # :accept, :accept_nonblock, :bind, :connect, :connect_nonblock, :getpeereid,
509
- # :ipv6only!, :listen, :recvfrom_nonblock, :sysaccept
510
- retries = 0
511
- logger.benchmark_info "Connected to #{server}" do
512
- host_name, port = server.split(":")
513
- port = port.to_i
588
+ def socket_connect(socket, address, timeout)
589
+ socket_address = Socket.pack_sockaddr_in(address.port, address.ip_address)
514
590
 
515
- address = Socket.getaddrinfo(host_name, nil, Socket::AF_INET, Socket::SOCK_STREAM).sample
516
- socket_address = Socket.pack_sockaddr_in(port, address[3])
591
+ # Timeout of -1 means wait forever for a connection
592
+ return socket.connect(socket_address) if timeout == -1
517
593
 
518
- begin
519
- @socket = Socket.new(Socket.const_get(address[0]), Socket::SOCK_STREAM, 0)
520
- @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) unless buffered
521
- if @connect_timeout == -1
522
- # Timeout of -1 means wait forever for a connection
523
- @socket.connect(socket_address)
524
- else
525
- begin
526
- @socket.connect_nonblock(socket_address)
527
- rescue Errno::EINPROGRESS
528
- end
529
- if IO.select(nil, [@socket], nil, @connect_timeout)
530
- begin
531
- @socket.connect_nonblock(socket_address)
532
- rescue Errno::EISCONN
533
- end
534
- else
535
- raise(Net::TCPClient::ConnectionTimeout.new("Timedout after #{@connect_timeout} seconds trying to connect to #{server}"))
536
- end
537
- end
538
- break
539
- rescue SystemCallError => exception
540
- if retries < @connect_retry_count && self.class.reconnect_on_errors.include?(exception.class)
541
- retries += 1
542
- logger.warn "Connection failure: #{exception.class}: #{exception.message}. Retry: #{retries}"
543
- sleep @connect_retry_interval
544
- retry
545
- end
546
- logger.error "Connection failure: #{exception.class}: #{exception.message}. Giving up after #{retries} retries"
547
- raise Net::TCPClient::ConnectionFailure.new("After #{retries} connection attempts to host '#{server}': #{exception.class}: #{exception.message}", @server, exception)
594
+ deadline = Time.now.utc + timeout
595
+ begin
596
+ non_blocking(socket, deadline) { socket.connect_nonblock(socket_address) }
597
+ rescue Errno::EISCONN
598
+ # Connection was successful.
599
+ rescue NonBlockingTimeout
600
+ raise ConnectionTimeout.new("Timed out after #{timeout} seconds trying to connect to #{address}")
601
+ rescue SystemCallError, IOError => exception
602
+ message = "#connect Connection failure connecting to '#{address.to_s}': #{exception.class}: #{exception.message}"
603
+ logger.error message if respond_to?(:logger)
604
+ raise ConnectionFailure.new(message, address.to_s, exception)
605
+ end
606
+ end
607
+
608
+ # Write to the socket
609
+ def socket_write(data, timeout)
610
+ if timeout < 0
611
+ socket.write(data)
612
+ else
613
+ deadline = Time.now.utc + timeout
614
+ non_blocking(socket, deadline) do
615
+ socket.write_nonblock(data)
548
616
  end
549
617
  end
550
- @server = server
618
+ rescue NonBlockingTimeout
619
+ logger.warn "#write Timeout after #{timeout} seconds" if respond_to?(:logger)
620
+ raise WriteTimeout.new("Timed out after #{timeout} seconds trying to write to #{address}")
621
+ rescue SystemCallError, IOError => exception
622
+ message = "#write Connection failure while writing to '#{address.to_s}': #{exception.class}: #{exception.message}"
623
+ logger.error message if respond_to?(:logger)
624
+ raise ConnectionFailure.new(message, address.to_s, exception)
551
625
  end
552
626
 
553
- # Try connecting to each server in the order supplied
554
- # The next server is tried if it cannot connect to the current one
555
- # After the last server a ConnectionFailure will be raised
556
- def connect_to_servers_in_order(servers)
557
- exception = nil
558
- servers.find do |server|
559
- begin
560
- connect_to_server(server)
561
- exception = nil
562
- true
563
- rescue Net::TCPClient::ConnectionFailure => exc
564
- exception = exc
565
- false
627
+ def socket_read(length, buffer, timeout)
628
+ result =
629
+ if timeout < 0
630
+ buffer.nil? ? socket.read(length) : socket.read(length, buffer)
631
+ else
632
+ deadline = Time.now.utc + timeout
633
+ non_blocking(socket, deadline) do
634
+ buffer.nil? ? socket.read_nonblock(length) : socket.read_nonblock(length, buffer)
635
+ end
566
636
  end
637
+
638
+ # EOF before all the data was returned
639
+ if result.nil? || (result.length < length)
640
+ logger.warn "#read server closed the connection before #{length} bytes were returned" if respond_to?(:logger)
641
+ raise ConnectionFailure.new('Connection lost while reading data', address.to_s, EOFError.new('end of file reached'))
567
642
  end
568
- # Raise Exception once it has also failed to connect to all servers
569
- raise(exception) if exception
643
+ result
644
+ rescue NonBlockingTimeout
645
+ logger.warn "#read Timeout after #{timeout} seconds" if respond_to?(:logger)
646
+ raise ReadTimeout.new("Timed out after #{timeout} seconds trying to read from #{address}")
647
+ rescue SystemCallError, IOError => exception
648
+ message = "#read Connection failure while reading data from '#{address.to_s}': #{exception.class}: #{exception.message}"
649
+ logger.error message if respond_to?(:logger)
650
+ raise ConnectionFailure.new(message, address.to_s, exception)
651
+ end
652
+
653
+ class NonBlockingTimeout< ::SocketError
570
654
  end
571
655
 
572
- # Return once data is ready to be ready
573
- # Raises Net::TCPClient::ReadTimeout if the timeout is exceeded
574
- def wait_for_data(timeout)
575
- return if timeout == -1
656
+ def non_blocking(socket, deadline)
657
+ yield
658
+ rescue IO::WaitReadable
659
+ time_remaining = check_time_remaining(deadline)
660
+ raise NonBlockingTimeout unless IO.select([socket], nil, nil, time_remaining)
661
+ retry
662
+ rescue IO::WaitWritable
663
+ time_remaining = check_time_remaining(deadline)
664
+ raise NonBlockingTimeout unless IO.select(nil, [socket], nil, time_remaining)
665
+ retry
666
+ end
667
+
668
+ def check_time_remaining(deadline)
669
+ time_remaining = deadline - Time.now.utc
670
+ raise NonBlockingTimeout if time_remaining < 0
671
+ time_remaining
672
+ end
673
+
674
+ # Try connecting to a single server
675
+ # Returns the connected socket
676
+ #
677
+ # Raises Net::TCPClient::ConnectionTimeout when the connection timeout has been exceeded
678
+ # Raises Net::TCPClient::ConnectionFailure
679
+ def ssl_connect(socket, address, timeout)
680
+ ssl_context = OpenSSL::SSL::SSLContext.new
681
+ ssl_context.set_params(ssl.is_a?(Hash) ? ssl : {})
682
+
683
+ ssl_socket = OpenSSL::SSL::SSLSocket.new(socket, ssl_context)
684
+ ssl_socket.sync_close = true
576
685
 
577
- ready = false
578
686
  begin
579
- ready = IO.select([@socket], nil, [@socket], timeout)
580
- rescue IOError => exception
581
- logger.warn "#read Connection failure while waiting for data: #{exception.class}: #{exception.message}"
582
- close if close_on_error
583
- raise Net::TCPClient::ConnectionFailure.new("#{exception.class}: #{exception.message}", @server, exception)
584
- rescue Exception
585
- # Close the connection on any other exception since the connection
586
- # will now be in an inconsistent state
587
- close if close_on_error
588
- raise
687
+ if timeout == -1
688
+ # Timeout of -1 means wait forever for a connection
689
+ ssl_socket.connect
690
+ else
691
+ deadline = Time.now.utc + timeout
692
+ begin
693
+ non_blocking(socket, deadline) { ssl_socket.connect_nonblock }
694
+ rescue Errno::EISCONN
695
+ # Connection was successful.
696
+ rescue NonBlockingTimeout
697
+ raise ConnectionTimeout.new("SSL handshake Timed out after #{timeout} seconds trying to connect to #{address.to_s}")
698
+ end
699
+ end
700
+ rescue SystemCallError, OpenSSL::SSL::SSLError, IOError => exception
701
+ message = "#connect SSL handshake failure with '#{address.to_s}': #{exception.class}: #{exception.message}"
702
+ logger.error message if respond_to?(:logger)
703
+ raise ConnectionFailure.new(message, address.to_s, exception)
589
704
  end
590
705
 
591
- unless ready
592
- close if close_on_error
593
- logger.warn "#read Timeout after #{timeout} seconds"
594
- raise Net::TCPClient::ReadTimeout.new("Timedout after #{timeout} seconds trying to read from #{@server}")
706
+ # Verify Peer certificate
707
+ ssl_verify(ssl_socket, address) if ssl_context.verify_mode != OpenSSL::SSL::VERIFY_NONE
708
+ ssl_socket
709
+ end
710
+
711
+ # Raises Net::TCPClient::ConnectionFailure if the peer certificate does not match its hostname
712
+ def ssl_verify(ssl_socket, address)
713
+ unless OpenSSL::SSL.verify_certificate_identity(ssl_socket.peer_cert, address.host_name)
714
+ ssl_socket.close
715
+ message = "#connect SSL handshake failed due to a hostname mismatch with '#{address.to_s}'"
716
+ logger.error message if respond_to?(:logger)
717
+ raise ConnectionFailure.new(message, address.to_s)
595
718
  end
596
719
  end
597
720