net_tcp_client 1.0.2 → 2.0.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.
@@ -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