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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9d6906da5ebe4657343ec1175664ee2f42b359eb
|
4
|
+
data.tar.gz: c816880d83f7d557390b00cbdfb5ed7c6a308512
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5d1aafb91baf376f68f291d5d18bc9d5569522cc2b093baf337f525b8060d3e632f6799ee64d8da12fc4db533f01a29059ec63144403da29e0f9884cd0f1a623
|
7
|
+
data.tar.gz: 32605f07229206ee29216ccfb5a2ecc74fe9c8c6d32459b7ec973fcd31f4541ab4ca710f148e6804254838265af364737449265908407f227100a96b761950af
|
data/README.md
CHANGED
@@ -1,43 +1,184 @@
|
|
1
1
|
# net_tcp_client
|
2
|
-
![](https://img.shields.io/gem/v/net_tcp_client.svg) ![](https://
|
2
|
+
[![Gem Version](https://img.shields.io/gem/v/net_tcp_client.svg)](https://rubygems.org/gems/net_tcp_client) [![Build Status](https://travis-ci.org/rocketjob/net_tcp_client.svg?branch=master)](https://travis-ci.org/rocketjob/net_tcp_client) [![Downloads](https://img.shields.io/gem/dt/net_tcp_client.svg)](https://rubygems.org/gems/net_tcp_client) [![License](https://img.shields.io/badge/license-Apache%202.0-brightgreen.svg)](http://opensource.org/licenses/Apache-2.0) ![](https://img.shields.io/badge/status-Production%20Ready-blue.svg) [![Gitter chat](https://img.shields.io/badge/IRC%20(gitter)-Support-brightgreen.svg)](https://gitter.im/rocketjob/support)
|
3
3
|
|
4
|
-
Net::TCPClient is a TCP Socket Client with
|
4
|
+
Net::TCPClient is a TCP Socket Client with automated failover, load balancing, retries and built-in timeouts.
|
5
5
|
|
6
6
|
* http://github.com/rocketjob/net_tcp_client
|
7
7
|
|
8
8
|
## Introduction
|
9
9
|
|
10
|
-
Net::TCPClient implements resilience features that many developers wish was
|
10
|
+
Net::TCPClient implements high availability and resilience features that many developers wish was
|
11
11
|
already included in the standard Ruby libraries.
|
12
12
|
|
13
|
-
With so many "client" libraries to servers such us memcache, MongoDB, Redis, etc.
|
14
|
-
their focus is on the communication formats and messaging interactions. As a result
|
15
|
-
adding resilience is usually an after thought.
|
16
|
-
|
17
|
-
More importantly the way that each client implements connection failure handling
|
18
|
-
varies dramatically. The purpose of this library is to extract the best
|
19
|
-
of all the socket error handling out there and create a consistent way of dealing
|
20
|
-
with connection failures.
|
21
|
-
|
22
13
|
Another important feature is that the _connect_ and _read_ API's use timeout's to
|
23
14
|
prevent a network issue from "hanging" the client program.
|
24
15
|
|
25
|
-
##
|
16
|
+
## Features
|
17
|
+
|
18
|
+
* Automated failover to another server.
|
19
|
+
* Load balancing across multiple servers.
|
20
|
+
* SSL and non-ssl connections.
|
21
|
+
* Connect Timeout.
|
22
|
+
* Read Timeout.
|
23
|
+
* Write Timeout.
|
24
|
+
* Fails over / load balances across all servers under a single DNS entry.
|
25
|
+
* Logging.
|
26
|
+
* Optional trace level logging of all data sent or received.
|
27
|
+
* Uses non blocking timeouts, instead of using threads such as used by the Timeout class.
|
28
|
+
* Additional exceptions to distinguish between connection failures and timeouts.
|
29
|
+
* Handshake callbacks.
|
30
|
+
* After a new connection has been established callbacks can be used
|
31
|
+
for handshakes such as authentication before data is sent.
|
32
|
+
|
33
|
+
### Example
|
34
|
+
|
35
|
+
~~~ruby
|
36
|
+
require 'net/tcp_client'
|
37
|
+
|
38
|
+
Net::TCPClient.connect(server: 'mydomain:3300') do |client|
|
39
|
+
client.send('Update the database')
|
40
|
+
response = client.read(20)
|
41
|
+
puts "Received: #{response}"
|
42
|
+
end
|
43
|
+
~~~
|
44
|
+
|
45
|
+
Enable SSL encryption:
|
46
|
+
|
47
|
+
~~~ruby
|
48
|
+
require 'net/tcp_client'
|
26
49
|
|
27
|
-
Net::TCPClient
|
50
|
+
Net::TCPClient.connect(server: 'mydomain:3300', ssl: true) do |client|
|
51
|
+
client.send('Update the database')
|
52
|
+
response = client.read(20)
|
53
|
+
puts "Received: #{response}"
|
54
|
+
end
|
55
|
+
~~~
|
28
56
|
|
29
|
-
|
30
|
-
that support automatic failover, re-connect and messaging retries.
|
57
|
+
## High Availability
|
31
58
|
|
32
|
-
|
59
|
+
Net::TCPClient automatically tries each server in turn, should it fail to connect, or
|
60
|
+
if the connection is lost the next server is tried immediately.
|
33
61
|
|
34
|
-
|
62
|
+
Net::TCPClient detects DNS entries that have multiple IP Addresses associated with them and
|
63
|
+
adds each of the ip addresses for the single DNS name to the list of servers to try to connect to.
|
35
64
|
|
36
|
-
|
65
|
+
If a server is unavailable, cannot connect, or the connection is lost, the next server is immediately
|
66
|
+
tried. Once all servers have been exhausted, it will keep trying to connect, starting with the
|
67
|
+
first server again.
|
37
68
|
|
38
|
-
|
39
|
-
|
69
|
+
When a connection is first established, and every time a connection is lost, Net::TCPClient
|
70
|
+
uses connection policies to determine which server to connect to.
|
71
|
+
|
72
|
+
## Load Balancing
|
73
|
+
|
74
|
+
Using the connection policies client TCP connections can be balanced across multiple servers.
|
75
|
+
|
76
|
+
## Connection Policies
|
77
|
+
|
78
|
+
#### Ordered
|
79
|
+
|
80
|
+
Servers are tried in the order they were supplied.
|
81
|
+
|
82
|
+
~~~ruby
|
83
|
+
tcp_client = Net::TCPClient.new(
|
84
|
+
servers: ['server1:3300', 'server2:3300', 'server3:3600']
|
85
|
+
)
|
86
|
+
~~~
|
87
|
+
|
88
|
+
The servers will tried in the following order:
|
89
|
+
`server1`, `server2`, `server3`
|
90
|
+
|
91
|
+
`:ordered` is the default, but can be explicitly defined follows:
|
92
|
+
|
93
|
+
~~~ruby
|
94
|
+
tcp_client = Net::TCPClient.new(
|
95
|
+
servers: ['server1:3300', 'server2:3300', 'server3:3600'],
|
96
|
+
policy: :ordered
|
97
|
+
)
|
98
|
+
~~~
|
99
|
+
|
100
|
+
#### Random
|
101
|
+
|
102
|
+
Servers are tried in a Random order.
|
103
|
+
|
104
|
+
~~~ruby
|
105
|
+
tcp_client = Net::TCPClient.new(
|
106
|
+
servers: ['server1:3300', 'server2:3300', 'server3:3600'],
|
107
|
+
policy: :ordered
|
108
|
+
)
|
109
|
+
~~~
|
110
|
+
|
111
|
+
No server is tried again until all of the others have been tried first.
|
112
|
+
|
113
|
+
Example run, the servers could be tried in the following order:
|
114
|
+
`server3`, `server1`, `server2`
|
115
|
+
|
116
|
+
#### Custom defined order
|
117
|
+
|
118
|
+
Supply your own custom order / load balancing algorithm for connecting to servers:
|
119
|
+
|
120
|
+
Example:
|
121
|
+
|
122
|
+
~~~ruby
|
123
|
+
tcp_client = Net::TCPClient.new(
|
124
|
+
servers: ['server1:3300', 'server2:3300', 'server3:3600'],
|
125
|
+
policy: -> addresses, count do
|
126
|
+
# Return nil after the last address has been tried so that retry logic can take over
|
127
|
+
if count <= address.size
|
128
|
+
addresses.sample
|
129
|
+
end
|
130
|
+
end
|
131
|
+
)
|
132
|
+
~~~
|
133
|
+
|
134
|
+
The above example returns addresses in random order without checking if a host name has been used before.
|
135
|
+
|
136
|
+
It is important to check the count so that once all servers have been tried, it should return nil so that
|
137
|
+
the retry logic can take over. Otherwise it will constantly try to connect to the servers without
|
138
|
+
the retry delays etc.
|
139
|
+
|
140
|
+
Example run, the servers could be tried in the following order:
|
141
|
+
`server3`, `server1`, `server3`
|
40
142
|
|
143
|
+
### Automatic Retry
|
144
|
+
|
145
|
+
If a connection cannot be established to any servers in the list Net::TCPClient will retry from the
|
146
|
+
first server. This retry behavior can be controlled using the following options:
|
147
|
+
|
148
|
+
* `connect_retry_count` [Fixnum]
|
149
|
+
* Number of times to retry connecting when a connection fails
|
150
|
+
* Default: 10
|
151
|
+
|
152
|
+
* `connect_retry_interval` [Float]
|
153
|
+
* Number of seconds between connection retry attempts after the first failed attempt
|
154
|
+
* Default: 0.5
|
155
|
+
|
156
|
+
* `retry_count` [Fixnum]
|
157
|
+
* Number of times to retry when calling #retry_on_connection_failure
|
158
|
+
* This is independent of :connect_retry_count which still applies with
|
159
|
+
* connection failures. This retry controls upto how many times to retry the
|
160
|
+
* supplied block should a connection failure occur during the block
|
161
|
+
* Default: 3
|
162
|
+
|
163
|
+
#### Note
|
164
|
+
|
165
|
+
A server will only be retried again using the retry controls above once all other servers in the
|
166
|
+
list have been exhausted.
|
167
|
+
|
168
|
+
This means that if a connection is lost to a server that it will try to connect to a different server,
|
169
|
+
not the same server unless it is the only server in the list.
|
170
|
+
|
171
|
+
## Tuning
|
172
|
+
|
173
|
+
If there are multiple servers in the list it is important to keep the `connect_timeout` low otherwise
|
174
|
+
it can take a long time to find the next available server.
|
175
|
+
|
176
|
+
## Retry on connection loss
|
177
|
+
|
178
|
+
To transparently handle when a connection is lost after it has been established
|
179
|
+
wrap calls that can be retried with `retry_on_connection_failure`.
|
180
|
+
|
181
|
+
~~~ruby
|
41
182
|
Net::TCPClient.connect(
|
42
183
|
server: 'localhost:3300',
|
43
184
|
connect_retry_interval: 0.1,
|
@@ -45,12 +186,57 @@ Net::TCPClient.connect(
|
|
45
186
|
) do |client|
|
46
187
|
# If the connection is lost, create a new one and retry the send
|
47
188
|
client.retry_on_connection_failure do
|
48
|
-
client.send('
|
189
|
+
client.send('How many users available?')
|
190
|
+
response = client.read(20)
|
191
|
+
puts "Received: #{response}"
|
49
192
|
end
|
50
|
-
response = client.read(20)
|
51
|
-
puts "Received: #{response}"
|
52
193
|
end
|
53
|
-
|
194
|
+
~~~
|
195
|
+
|
196
|
+
If the connection is lost during either the `send` or the `read` above the
|
197
|
+
entire block will be re-tried once the connection has been re-stablished.
|
198
|
+
|
199
|
+
## Callbacks
|
200
|
+
|
201
|
+
Any time a connection has been established a callback can be called to handle activities such as:
|
202
|
+
|
203
|
+
* Initialize per connection session sequence numbers.
|
204
|
+
* Pass authentication information to the server.
|
205
|
+
* Perform a handshake with the server.
|
206
|
+
|
207
|
+
#### Authentication example:
|
208
|
+
|
209
|
+
~~~ruby
|
210
|
+
tcp_client = Net::TCPClient.new(
|
211
|
+
servers: ['server1:3300', 'server2:3300', 'server3:3600'],
|
212
|
+
on_connect: -> do |client|
|
213
|
+
client.send('My username and password')
|
214
|
+
result = client.read(2)
|
215
|
+
raise "Authentication failed" if result != 'OK'
|
216
|
+
end
|
217
|
+
)
|
218
|
+
~~~
|
219
|
+
|
220
|
+
#### Per connection sequence number example:
|
221
|
+
|
222
|
+
~~~ruby
|
223
|
+
tcp_client = Net::TCPClient.new(
|
224
|
+
servers: ['server1:3300', 'server2:3300', 'server3:3600'],
|
225
|
+
on_connect: -> do |client|
|
226
|
+
# Set the sequence number to 0
|
227
|
+
user_data = 0
|
228
|
+
end
|
229
|
+
)
|
230
|
+
|
231
|
+
tcp_client.retry_on_connection_failure do
|
232
|
+
# Send with the sequence number
|
233
|
+
tcp_client.send("#{tcp_client.user_data} hello")
|
234
|
+
result = tcp_client.receive(30)
|
235
|
+
|
236
|
+
# Increment sequence number after every call to the server
|
237
|
+
tcp_client.user_data += 1
|
238
|
+
end
|
239
|
+
~~~
|
54
240
|
|
55
241
|
## Project Status
|
56
242
|
|
@@ -64,34 +250,46 @@ test on a daily basis, including connections over the internet between remote da
|
|
64
250
|
|
65
251
|
gem install net_tcp_client
|
66
252
|
|
67
|
-
|
253
|
+
To enable logging add [Semantic Logger](http://rocketjob.github.io/semantic_logger):
|
68
254
|
|
69
255
|
gem install semantic_logger
|
70
256
|
|
71
257
|
Or, add the following lines to you `Gemfile`:
|
72
258
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
259
|
+
~~~ruby
|
260
|
+
gem 'semantic_logger'
|
261
|
+
gem 'net_tcp_client'
|
262
|
+
~~~
|
77
263
|
|
78
264
|
To configure a stand-alone application for Semantic Logger:
|
79
265
|
|
80
|
-
|
266
|
+
~~~ruby
|
81
267
|
require 'semantic_logger'
|
82
268
|
|
83
269
|
# Set the global default log level
|
84
270
|
SemanticLogger.default_level = :trace
|
85
271
|
|
86
272
|
# Log to a file, and use the colorized formatter
|
87
|
-
SemanticLogger.add_appender('development.log',
|
88
|
-
|
273
|
+
SemanticLogger.add_appender(file_name: 'development.log', formatter: :color)
|
274
|
+
~~~
|
89
275
|
|
90
276
|
If running Rails, see: [Semantic Logger Rails](http://rocketjob.github.io/semantic_logger/rails.html)
|
91
277
|
|
92
|
-
|
278
|
+
### Support
|
93
279
|
|
94
|
-
|
280
|
+
Join the [Gitter chat session](https://gitter.im/rocketjob/support) if you have any questions.
|
281
|
+
|
282
|
+
Issues / bugs can be reported via [Github issues](https://github.com/rocketjob/net_tcp_client/issues).
|
283
|
+
|
284
|
+
### Upgrading to V2
|
285
|
+
|
286
|
+
The following breaking changes have been made with V2:
|
287
|
+
* The Connection timeout default is now 10 seconds, was 30 seconds.
|
288
|
+
* To enable logging, add gem semantic_logger.
|
289
|
+
* The :logger option has been removed.
|
290
|
+
* Deprecated option and attribute :server_selector has been removed.
|
291
|
+
|
292
|
+
### Upgrading from ResilientSocket ![](https://img.shields.io/gem/dt/resilient_socket.svg)
|
95
293
|
|
96
294
|
ResilientSocket::TCPClient has been renamed to Net::TCPClient.
|
97
295
|
The API is exactly the same, just with a new namespace. Please upgrade to the new
|
@@ -101,8 +299,8 @@ with `Net::TCPClient` in your code.
|
|
101
299
|
## Supports
|
102
300
|
|
103
301
|
Tested and supported on the following Ruby platforms:
|
104
|
-
- Ruby 1
|
105
|
-
- JRuby 1.7, 9.0 and above
|
302
|
+
- Ruby 2.1, 2.2, 2.3 and above
|
303
|
+
- JRuby 1.7.23, 9.0 and above
|
106
304
|
- Rubinius 2.5 and above
|
107
305
|
|
108
306
|
There is a soft dependency on [Semantic Logger](http://github.com/rocketjob/semantic_logger). It will use SemanticLogger only if
|
@@ -112,14 +310,12 @@ it is already available, otherwise any other standard Ruby logger can be used.
|
|
112
310
|
|
113
311
|
Be sure to place the `semantic_logger` gem dependency before `net_tcp_client` in your Gemfile.
|
114
312
|
|
115
|
-
## Versioning
|
116
|
-
|
117
|
-
This project adheres to [Semantic Versioning](http://semver.org/).
|
118
|
-
|
119
313
|
## Author
|
120
314
|
|
121
315
|
[Reid Morrison](https://github.com/reidmorrison)
|
122
316
|
|
317
|
+
[Contributors](https://github.com/rocketjob/net_tcp_client/graphs/contributors)
|
318
|
+
|
123
319
|
## Versioning
|
124
320
|
|
125
321
|
This project uses [Semantic Versioning](http://semver.org/).
|
data/lib/net/tcp_client.rb
CHANGED
@@ -1,6 +1,22 @@
|
|
1
1
|
require 'socket'
|
2
|
+
# Load SemanticLogger if available
|
3
|
+
begin
|
4
|
+
require 'semantic_logger'
|
5
|
+
rescue LoadError
|
6
|
+
end
|
2
7
|
require 'net/tcp_client/version'
|
8
|
+
require 'net/tcp_client/address'
|
3
9
|
require 'net/tcp_client/exceptions'
|
4
10
|
require 'net/tcp_client/tcp_client'
|
5
|
-
|
6
|
-
|
11
|
+
|
12
|
+
# @formatter:off
|
13
|
+
module Net
|
14
|
+
class TCPClient
|
15
|
+
module Policy
|
16
|
+
autoload :Base, 'net/tcp_client/policy/base.rb'
|
17
|
+
autoload :Custom, 'net/tcp_client/policy/custom.rb'
|
18
|
+
autoload :Ordered, 'net/tcp_client/policy/ordered.rb'
|
19
|
+
autoload :Random, 'net/tcp_client/policy/random.rb'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'ipaddr'
|
3
|
+
module Net
|
4
|
+
class TCPClient
|
5
|
+
# Host name, ip address and port to connect to
|
6
|
+
class Address
|
7
|
+
attr_accessor :host_name, :ip_address, :port
|
8
|
+
|
9
|
+
# Returns [Array<String>] ip addresses for the supplied DNS entry
|
10
|
+
# Returns dns_name if it is already an IP Address
|
11
|
+
def self.ip_addresses(dns_name)
|
12
|
+
ips = []
|
13
|
+
Socket.getaddrinfo(dns_name, nil, Socket::AF_INET, Socket::SOCK_STREAM).each do |s|
|
14
|
+
ips << s[3] if s[0] == 'AF_INET'
|
15
|
+
end
|
16
|
+
ips.uniq
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns [Array<Net::TCPClient::Address>] addresses for a given DNS / host name.
|
20
|
+
# The Addresses will contain the resolved ip address, host name, and port number.
|
21
|
+
#
|
22
|
+
# Note:
|
23
|
+
# Multiple ip addresses will be returned when a DNS entry has multiple ip addresses associated with it.
|
24
|
+
def self.addresses(dns_name, port)
|
25
|
+
ip_addresses(dns_name).collect { |ip| new(dns_name, ip, port) }
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns [Array<Net::TCPClient::Address>] addresses for a list of DNS / host name's
|
29
|
+
# that are paired with their numbers
|
30
|
+
#
|
31
|
+
# server_name should be either a host_name, or ip address combined with a port:
|
32
|
+
# "host_name:1234"
|
33
|
+
# "192.168.1.10:80"
|
34
|
+
def self.addresses_for_server_name(server_name)
|
35
|
+
dns_name, port = server_name.split(':')
|
36
|
+
port = port.to_i
|
37
|
+
raise(ArgumentError, "Invalid host_name: #{server_name.inspect}. Must be formatted as 'host_name:1234' or '192.168.1.10:80'") unless dns_name && (port > 0)
|
38
|
+
addresses(dns_name, port)
|
39
|
+
end
|
40
|
+
|
41
|
+
def initialize(host_name, ip_address, port)
|
42
|
+
@host_name = host_name
|
43
|
+
@ip_address = ip_address
|
44
|
+
@port = port.to_i
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_s
|
48
|
+
"#{host_name}[#{ip_address}]:#{port}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
@@ -1,14 +1,16 @@
|
|
1
1
|
module Net
|
2
2
|
class TCPClient
|
3
3
|
|
4
|
-
class ConnectionTimeout < ::SocketError
|
4
|
+
class ConnectionTimeout < ::SocketError
|
5
5
|
end
|
6
|
-
class ReadTimeout < ::SocketError
|
6
|
+
class ReadTimeout < ::SocketError
|
7
|
+
end
|
8
|
+
class WriteTimeout < ::SocketError
|
7
9
|
end
|
8
10
|
|
9
11
|
# Raised by ResilientSocket whenever a Socket connection failure has occurred
|
10
12
|
class ConnectionFailure < ::SocketError
|
11
|
-
# Returns the
|
13
|
+
# Returns the host name and port against which the connection failure occurred
|
12
14
|
attr_reader :server
|
13
15
|
|
14
16
|
# Returns the original exception that caused the connection failure
|