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.
@@ -0,0 +1,5 @@
1
+ module Net
2
+ class TCPClient #:nodoc
3
+ VERSION = "1.0.0"
4
+ end
5
+ end
@@ -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