net_tcp_client 1.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 +7 -0
- data/LICENSE.txt +201 -0
- data/README.md +101 -0
- data/Rakefile +28 -0
- data/lib/net/tcp_client.rb +5 -0
- data/lib/net/tcp_client/exceptions.rb +35 -0
- data/lib/net/tcp_client/logging.rb +191 -0
- data/lib/net/tcp_client/tcp_client.rb +604 -0
- data/lib/net/tcp_client/version.rb +5 -0
- data/test/simple_tcp_server.rb +114 -0
- data/test/tcp_client_test.rb +190 -0
- metadata +58 -0
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'socket'
|
3
|
+
require 'bson'
|
4
|
+
require 'semantic_logger'
|
5
|
+
|
6
|
+
# Read the bson document, returning nil if the IO is closed
|
7
|
+
# before receiving any data or a complete BSON document
|
8
|
+
def read_bson_document(io)
|
9
|
+
bytebuf = BSON::ByteBuffer.new
|
10
|
+
# Read 4 byte size of following BSON document
|
11
|
+
bytes = io.read(4)
|
12
|
+
return unless bytes
|
13
|
+
# Read BSON document
|
14
|
+
sz = bytes.unpack("V")[0]
|
15
|
+
bytebuf.append!(bytes)
|
16
|
+
bytes = io.read(sz-4)
|
17
|
+
return unless bytes
|
18
|
+
bytebuf.append!(bytes)
|
19
|
+
return BSON.deserialize(bytebuf)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Simple single threaded server for testing purposes using a local socket
|
23
|
+
# Sends and receives BSON Messages
|
24
|
+
class SimpleTCPServer
|
25
|
+
attr_reader :thread
|
26
|
+
def initialize(port = 2000)
|
27
|
+
start(port)
|
28
|
+
end
|
29
|
+
|
30
|
+
def start(port)
|
31
|
+
@server = TCPServer.open(port)
|
32
|
+
@logger = SemanticLogger::Logger.new(self.class)
|
33
|
+
|
34
|
+
@thread = Thread.new do
|
35
|
+
loop do
|
36
|
+
@logger.debug "Waiting for a client to connect"
|
37
|
+
|
38
|
+
# Wait for a client to connect
|
39
|
+
on_request(@server.accept)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def stop
|
45
|
+
if @thread
|
46
|
+
@thread.kill
|
47
|
+
@thread.join
|
48
|
+
@thread = nil
|
49
|
+
end
|
50
|
+
begin
|
51
|
+
@server.close if @server
|
52
|
+
rescue IOError
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Called for each message received from the client
|
57
|
+
# Returns a Hash that is sent back to the caller
|
58
|
+
def on_message(message)
|
59
|
+
case message['action']
|
60
|
+
when 'test1'
|
61
|
+
{ 'result' => 'test1' }
|
62
|
+
when 'sleep'
|
63
|
+
sleep message['duration'] || 1
|
64
|
+
{ 'result' => 'sleep' }
|
65
|
+
when 'fail'
|
66
|
+
if message['attempt'].to_i >= 2
|
67
|
+
{ 'result' => 'fail' }
|
68
|
+
else
|
69
|
+
nil
|
70
|
+
end
|
71
|
+
else
|
72
|
+
{ 'result' => "Unknown action: #{message['action']}" }
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Called for each client connection
|
77
|
+
# In a real server each request would be handled in a separate thread
|
78
|
+
def on_request(client)
|
79
|
+
@logger.debug "Client connected, waiting for data from client"
|
80
|
+
|
81
|
+
while(request = read_bson_document(client)) do
|
82
|
+
@logger.debug "\n****************** Received request"
|
83
|
+
@logger.trace 'Request', request
|
84
|
+
break unless request
|
85
|
+
|
86
|
+
if reply = on_message(request)
|
87
|
+
@logger.debug "Sending Reply"
|
88
|
+
@logger.trace 'Reply', reply
|
89
|
+
client.print(BSON.serialize(reply))
|
90
|
+
else
|
91
|
+
@logger.debug "Closing client since no reply is being sent back"
|
92
|
+
@server.close
|
93
|
+
client.close
|
94
|
+
@logger.debug "Server closed"
|
95
|
+
#@thread.kill
|
96
|
+
@logger.debug "thread killed"
|
97
|
+
start(2000)
|
98
|
+
@logger.debug "Server Restarted"
|
99
|
+
break
|
100
|
+
end
|
101
|
+
end
|
102
|
+
# Disconnect from the client
|
103
|
+
client.close
|
104
|
+
@logger.debug "Disconnected from the client"
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
|
109
|
+
if $0 == __FILE__
|
110
|
+
SemanticLogger.default_level = :trace
|
111
|
+
SemanticLogger.add_appender(STDOUT)
|
112
|
+
server = SimpleTCPServer.new(2000)
|
113
|
+
server.thread.join
|
114
|
+
end
|
@@ -0,0 +1,190 @@
|
|
1
|
+
# Allow test to be run in-place without requiring a gem install
|
2
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
|
3
|
+
$LOAD_PATH.unshift File.dirname(__FILE__)
|
4
|
+
|
5
|
+
require 'rubygems'
|
6
|
+
require 'test/unit'
|
7
|
+
require 'shoulda'
|
8
|
+
require 'socket'
|
9
|
+
require 'net/tcp_client'
|
10
|
+
require 'simple_tcp_server'
|
11
|
+
|
12
|
+
SemanticLogger.default_level = :trace
|
13
|
+
SemanticLogger.add_appender('test.log')
|
14
|
+
|
15
|
+
# Unit Test for Net::TCPClient
|
16
|
+
class TCPClientTest < Test::Unit::TestCase
|
17
|
+
context Net::TCPClient do
|
18
|
+
|
19
|
+
context "without server" do
|
20
|
+
should "raise exception when cannot reach server after 5 retries" do
|
21
|
+
exception = assert_raise Net::TCPClient::ConnectionFailure do
|
22
|
+
Net::TCPClient.new(
|
23
|
+
:server => 'localhost:3300',
|
24
|
+
:connect_retry_interval => 0.1,
|
25
|
+
:connect_retry_count => 5)
|
26
|
+
end
|
27
|
+
assert_match /After 5 connection attempts to host 'localhost:3300': Errno::ECONNREFUSED/, exception.message
|
28
|
+
end
|
29
|
+
|
30
|
+
should "timeout on connect" do
|
31
|
+
# Create a TCP Server, but do not respond to connections
|
32
|
+
server = TCPServer.open(2001)
|
33
|
+
|
34
|
+
exception = assert_raise Net::TCPClient::ConnectionTimeout do
|
35
|
+
1000.times do
|
36
|
+
Net::TCPClient.new(
|
37
|
+
:server => 'localhost:2001',
|
38
|
+
:connect_timeout => 0.5,
|
39
|
+
:connect_retry_count => 3
|
40
|
+
)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
assert_match /Timedout after/, exception.message
|
44
|
+
server.close
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
context "with server" do
|
50
|
+
setup do
|
51
|
+
@server = SimpleTCPServer.new(2000)
|
52
|
+
@server_name = 'localhost:2000'
|
53
|
+
end
|
54
|
+
|
55
|
+
teardown do
|
56
|
+
@server.stop if @server
|
57
|
+
end
|
58
|
+
|
59
|
+
context "without client connection" do
|
60
|
+
should "timeout on first receive and then successfully read the response" do
|
61
|
+
@read_timeout = 3.0
|
62
|
+
# Need a custom client that does not auto close on error:
|
63
|
+
@client = Net::TCPClient.new(
|
64
|
+
:server => @server_name,
|
65
|
+
:read_timeout => @read_timeout,
|
66
|
+
:close_on_error => false
|
67
|
+
)
|
68
|
+
|
69
|
+
request = { 'action' => 'sleep', 'duration' => @read_timeout + 0.5}
|
70
|
+
@client.write(BSON.serialize(request))
|
71
|
+
|
72
|
+
exception = assert_raise Net::TCPClient::ReadTimeout do
|
73
|
+
# Read 4 bytes from server
|
74
|
+
@client.read(4)
|
75
|
+
end
|
76
|
+
assert_equal false, @client.close_on_error
|
77
|
+
assert @client.alive?, "The client connection is not alive after the read timed out with :close_on_error => false"
|
78
|
+
assert_match /Timedout after #{@read_timeout} seconds trying to read from #{@server_name}/, exception.message
|
79
|
+
reply = read_bson_document(@client)
|
80
|
+
assert_equal 'sleep', reply['result']
|
81
|
+
@client.close
|
82
|
+
end
|
83
|
+
|
84
|
+
should "support infinite timeout" do
|
85
|
+
@client = Net::TCPClient.new(
|
86
|
+
:server => @server_name,
|
87
|
+
:connect_timeout => -1
|
88
|
+
)
|
89
|
+
request = { 'action' => 'test1' }
|
90
|
+
@client.write(BSON.serialize(request))
|
91
|
+
reply = read_bson_document(@client)
|
92
|
+
assert_equal 'test1', reply['result']
|
93
|
+
@client.close
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
context "with client connection" do
|
98
|
+
setup do
|
99
|
+
@read_timeout = 3.0
|
100
|
+
@client = Net::TCPClient.new(
|
101
|
+
:server => @server_name,
|
102
|
+
:read_timeout => @read_timeout
|
103
|
+
)
|
104
|
+
assert @client.alive?
|
105
|
+
assert_equal true, @client.close_on_error
|
106
|
+
end
|
107
|
+
|
108
|
+
def teardown
|
109
|
+
if @client
|
110
|
+
@client.close
|
111
|
+
assert !@client.alive?
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
should "successfully send and receive data" do
|
116
|
+
request = { 'action' => 'test1' }
|
117
|
+
@client.write(BSON.serialize(request))
|
118
|
+
reply = read_bson_document(@client)
|
119
|
+
assert_equal 'test1', reply['result']
|
120
|
+
end
|
121
|
+
|
122
|
+
should "timeout on receive" do
|
123
|
+
request = { 'action' => 'sleep', 'duration' => @read_timeout + 0.5}
|
124
|
+
@client.write(BSON.serialize(request))
|
125
|
+
|
126
|
+
exception = assert_raise Net::TCPClient::ReadTimeout do
|
127
|
+
# Read 4 bytes from server
|
128
|
+
@client.read(4)
|
129
|
+
end
|
130
|
+
# Due to :close_on_error => true, a timeout will close the connection
|
131
|
+
# to prevent use of a socket connection in an inconsistent state
|
132
|
+
assert_equal false, @client.alive?
|
133
|
+
assert_match /Timedout after #{@read_timeout} seconds trying to read from #{@server_name}/, exception.message
|
134
|
+
end
|
135
|
+
|
136
|
+
should "retry on connection failure" do
|
137
|
+
attempt = 0
|
138
|
+
reply = @client.retry_on_connection_failure do
|
139
|
+
request = { 'action' => 'fail', 'attempt' => (attempt+=1) }
|
140
|
+
@client.write(BSON.serialize(request))
|
141
|
+
# Note: Do not put the read in this block if it should never send the
|
142
|
+
# same request twice to the server
|
143
|
+
read_bson_document(@client)
|
144
|
+
end
|
145
|
+
assert_equal 'fail', reply['result']
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
149
|
+
|
150
|
+
context "without client connection" do
|
151
|
+
should "connect to second server when first is down" do
|
152
|
+
client = Net::TCPClient.new(
|
153
|
+
:servers => ['localhost:1999', @server_name],
|
154
|
+
:read_timeout => 3
|
155
|
+
)
|
156
|
+
assert_equal @server_name, client.server
|
157
|
+
|
158
|
+
request = { 'action' => 'test1' }
|
159
|
+
client.write(BSON.serialize(request))
|
160
|
+
reply = read_bson_document(client)
|
161
|
+
assert_equal 'test1', reply['result']
|
162
|
+
|
163
|
+
client.close
|
164
|
+
end
|
165
|
+
|
166
|
+
should "call on_connect after connection" do
|
167
|
+
client = Net::TCPClient.new(
|
168
|
+
:server => @server_name,
|
169
|
+
:read_timeout => 3,
|
170
|
+
:on_connect => Proc.new do |socket|
|
171
|
+
# Reset user_data on each connection
|
172
|
+
socket.user_data = { :sequence => 1 }
|
173
|
+
end
|
174
|
+
)
|
175
|
+
assert_equal @server_name, client.server
|
176
|
+
assert_equal 1, client.user_data[:sequence]
|
177
|
+
|
178
|
+
request = { 'action' => 'test1' }
|
179
|
+
client.write(BSON.serialize(request))
|
180
|
+
reply = read_bson_document(client)
|
181
|
+
assert_equal 'test1', reply['result']
|
182
|
+
|
183
|
+
client.close
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
end
|
188
|
+
|
189
|
+
end
|
190
|
+
end
|
metadata
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: net_tcp_client
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Reid Morrison
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-09-25 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Net::TCPClient implements resilience features that most developers wish
|
14
|
+
was already included in the standard Ruby libraries.
|
15
|
+
email:
|
16
|
+
- reidmo@gmail.com
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- LICENSE.txt
|
22
|
+
- README.md
|
23
|
+
- Rakefile
|
24
|
+
- lib/net/tcp_client.rb
|
25
|
+
- lib/net/tcp_client/exceptions.rb
|
26
|
+
- lib/net/tcp_client/logging.rb
|
27
|
+
- lib/net/tcp_client/tcp_client.rb
|
28
|
+
- lib/net/tcp_client/version.rb
|
29
|
+
- test/simple_tcp_server.rb
|
30
|
+
- test/tcp_client_test.rb
|
31
|
+
homepage: https://github.com/reidmorrison/net_tcp_client
|
32
|
+
licenses:
|
33
|
+
- Apache License V2.0
|
34
|
+
metadata: {}
|
35
|
+
post_install_message:
|
36
|
+
rdoc_options: []
|
37
|
+
require_paths:
|
38
|
+
- lib
|
39
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
requirements: []
|
50
|
+
rubyforge_project:
|
51
|
+
rubygems_version: 2.2.2
|
52
|
+
signing_key:
|
53
|
+
specification_version: 4
|
54
|
+
summary: Net::TCPClient is a TCP Socket Client with built-in timeouts, retries, and
|
55
|
+
logging
|
56
|
+
test_files:
|
57
|
+
- test/simple_tcp_server.rb
|
58
|
+
- test/tcp_client_test.rb
|