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