net_tcp_client 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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