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.
- checksums.yaml +4 -4
- data/README.md +237 -41
- data/lib/net/tcp_client.rb +18 -2
- data/lib/net/tcp_client/address.rb +53 -0
- data/lib/net/tcp_client/exceptions.rb +5 -3
- data/lib/net/tcp_client/policy/base.rb +36 -0
- data/lib/net/tcp_client/policy/custom.rb +39 -0
- data/lib/net/tcp_client/policy/ordered.rb +14 -0
- data/lib/net/tcp_client/policy/random.rb +14 -0
- data/lib/net/tcp_client/tcp_client.rb +332 -209
- data/lib/net/tcp_client/version.rb +1 -1
- data/test/address_test.rb +91 -0
- data/test/policy/custom_policy_test.rb +42 -0
- data/test/policy/ordered_policy_test.rb +36 -0
- data/test/policy/random_policy_test.rb +46 -0
- data/test/simple_tcp_server.rb +36 -9
- data/test/ssl_files/ca.pem +19 -0
- data/test/ssl_files/localhost-server-key.pem +27 -0
- data/test/ssl_files/localhost-server.pem +18 -0
- data/test/tcp_client_test.rb +207 -143
- data/test/test_helper.rb +1 -2
- metadata +23 -5
- data/lib/net/tcp_client/logging.rb +0 -193
@@ -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:
|
119
|
+
# Default: 10 seconds
|
129
120
|
#
|
130
|
-
# :
|
131
|
-
#
|
132
|
-
#
|
133
|
-
#
|
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
|
-
# :
|
137
|
-
#
|
138
|
-
#
|
139
|
-
#
|
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
|
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
|
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
|
-
# :
|
174
|
-
#
|
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
|
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
|
-
# :
|
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
|
-
#
|
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
|
-
@
|
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
|
-
@
|
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
|
-
@
|
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
|
-
|
247
|
-
|
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
|
-
|
281
|
-
|
282
|
-
|
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
|
-
#
|
295
|
-
|
296
|
-
|
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
|
-
|
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
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
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
|
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
|
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
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
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
|
-
|
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
|
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.
|
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
|
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
|
-
|
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
|
-
|
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
|
524
|
+
return false if socket.nil? || closed?
|
482
525
|
|
483
|
-
if IO.select([
|
484
|
-
|
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
|
-
|
493
|
-
|
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
|
-
#
|
501
|
-
# Returns the connected
|
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
|
506
|
-
|
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
|
-
|
516
|
-
|
591
|
+
# Timeout of -1 means wait forever for a connection
|
592
|
+
return socket.connect(socket_address) if timeout == -1
|
517
593
|
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
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
|
-
|
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
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
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
|
-
|
569
|
-
|
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
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
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
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
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
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
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
|
|