tcp-client 0.1.3 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bc62bca2078a00db24dc861aa876a6e2acac79dd1f2eb0cafbd29840d5169704
4
- data.tar.gz: c784e00a152bcf7ac5c1246517483a39f55beb3f7591881be58a4edeb847097e
3
+ metadata.gz: 2791642003fb16623a27988401ff3bb532194032a5bbd4f62b853aae94790055
4
+ data.tar.gz: 030a2b3ebecf1334dea84e2c8f18aecd3b900c7c467c10c624c1354429a66195
5
5
  SHA512:
6
- metadata.gz: 77ff9adf39b16aab0ca83cd77c01f688a77efd675c287095e5b67c1b52f89cf0528ddbc0a1c206330703cf430c4f457ddbdcb369f5a0a8f57a6c143ee5266f7e
7
- data.tar.gz: 27df4b59116a68cf8232f471214e305d74f650b4e1be6cbd59302660f8d2998beef1fd7c382079311f9139c3465bda20981f3ff0993659f74807c00bb3942959
6
+ metadata.gz: e4ef14d49cd323a418190e62bc190af01fcc9be1b7008b7fd27c8fa7b8779ae29947045a660080e9403ba1d932e1318a6f98c819006d816f91eb093caf6d7873
7
+ data.tar.gz: 29617e860ad0b419638b54a8258541ccf3c668530a38aa2e629badcb373b94df71c33e538d54faf12d732ffd51670e1ad57cc8fabcaac11bb2d4de3d28cd29b1
data/README.md CHANGED
@@ -12,22 +12,18 @@ require 'tcp-client'
12
12
 
13
13
  TCPClient.configure do |cfg|
14
14
  cfg.connect_timeout = 1 # second to connect the server
15
- cfg.write_timeout = 0.25 # seconds to write a single data junk
16
- cfg.read_timeout = 0.5 # seconds to read some bytes
17
15
  cfg.ssl_params = { ssl_version: :TLSv1_2 } # use TLS 1.2
18
16
  end
19
17
 
20
- # the following request sequence is not allowed to last longer than 2 seconds:
21
- # 1 second to connect (incl. SSL handshake etc.)
22
- # + 0.25 seconds to write data
23
- # + 0.5 seconds to read a response
24
-
25
18
  TCPClient.open('www.google.com:443') do |client|
26
- # simple HTTP get request
27
- pp client.write("GET / HTTP/1.1\r\nHost: google.com\r\n\r\n")
28
-
29
- # read "HTTP/1.1 " + 3 byte HTTP status code
30
- pp client.read(12)
19
+ # query should not last longer than 0.5 seconds
20
+ client.with_deadline(0.5) do
21
+ # simple HTTP get request
22
+ pp client.write("GET / HTTP/1.1\r\nHost: google.com\r\n\r\n")
23
+
24
+ # read "HTTP/1.1 " + 3 byte HTTP status code
25
+ pp client.read(12)
26
+ end
31
27
  end
32
28
  ```
33
29
 
data/lib/tcp-client.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'tcp-client/errors'
3
4
  require_relative 'tcp-client/address'
4
5
  require_relative 'tcp-client/tcp_socket'
5
6
  require_relative 'tcp-client/ssl_socket'
@@ -8,30 +9,9 @@ require_relative 'tcp-client/default_configuration'
8
9
  require_relative 'tcp-client/version'
9
10
 
10
11
  class TCPClient
11
- class NoOpenSSL < RuntimeError
12
- def self.raise!
13
- raise(self, 'OpenSSL is not avail', caller(1))
14
- end
15
- end
16
-
17
- class NotConnected < SocketError
18
- def self.raise!(reason)
19
- raise(self, "client not connected - #{reason}", caller(1))
20
- end
21
- end
22
-
23
- TimeoutError = Class.new(IOError)
24
- ConnectTimeoutError = Class.new(TimeoutError)
25
- ReadTimeoutError = Class.new(TimeoutError)
26
- WriteTimeoutError = Class.new(TimeoutError)
27
-
28
- Timeout = TimeoutError # backward compatibility
29
- deprecate_constant(:Timeout)
30
-
31
12
  def self.open(addr, configuration = Configuration.default)
32
- addr = Address.new(addr)
33
13
  client = new
34
- client.connect(addr, configuration)
14
+ client.connect(Address.new(addr), configuration)
35
15
  block_given? ? yield(client) : client
36
16
  ensure
37
17
  client&.close if block_given?
@@ -40,50 +20,88 @@ class TCPClient
40
20
  attr_reader :address
41
21
 
42
22
  def initialize
43
- @socket = @address = @write_timeout = @read_timeout = nil
23
+ @socket = @address = @deadline = @cfg = nil
44
24
  end
45
25
 
46
26
  def to_s
47
27
  @address ? @address.to_s : ''
48
28
  end
49
29
 
50
- def connect(addr, configuration)
30
+ def connect(addr, configuration, exception: nil)
51
31
  close
52
32
  NoOpenSSL.raise! if configuration.ssl? && !defined?(SSLSocket)
53
33
  @address = Address.new(addr)
54
- @socket = TCPSocket.new(@address, configuration, ConnectTimeoutError)
55
- configuration.ssl? &&
56
- @socket =
57
- SSLSocket.new(@socket, @address, configuration, ConnectTimeoutError)
58
- @write_timeout = configuration.write_timeout
59
- @read_timeout = configuration.read_timeout
34
+ @cfg = configuration.dup
35
+ exception ||= configuration.connect_timeout_error
36
+ @socket = TCPSocket.new(@address, @cfg, exception)
37
+ @cfg.ssl? &&
38
+ @socket = SSLSocket.new(@socket, @address, configuration, exception)
60
39
  self
61
40
  end
62
41
 
63
42
  def close
64
- socket, @socket = @socket, nil
65
- socket&.close
43
+ @socket&.close
66
44
  self
67
45
  rescue IOError
68
46
  self
47
+ ensure
48
+ @socket = @deadline = nil
69
49
  end
70
50
 
71
51
  def closed?
72
52
  @socket.nil? || @socket.closed?
73
53
  end
74
54
 
75
- def read(nbytes, timeout: @read_timeout)
76
- NotConnected.raise!(self) if closed?
77
- @socket.read(nbytes, timeout: timeout, exception: ReadTimeoutError)
55
+ def with_deadline(timeout)
56
+ previous_deadline = @deadline
57
+ NoBlockGiven.raise! unless block_given?
58
+ tm = timeout&.to_f
59
+ InvalidDeadLine.raise! unless tm&.positive?
60
+ @deadline = Time.now + tm
61
+ yield(self)
62
+ ensure
63
+ @deadline = previous_deadline
64
+ end
65
+
66
+ def read(nbytes, timeout: nil, exception: nil)
67
+ NotConnected.raise! if closed?
68
+ timeout.nil? && @deadline and
69
+ return read_with_deadline(nbytes, @deadline, exception)
70
+ timeout = (timeout || @cfg.read_timeout).to_f
71
+ return @socket.read(nbytes) unless timeout.positive?
72
+ read_with_deadline(nbytes, Time.now + timeout, exception)
78
73
  end
79
74
 
80
- def write(*msg, timeout: @write_timeout)
81
- NotConnected.raise!(self) if closed?
82
- @socket.write(*msg, timeout: timeout, exception: WriteTimeoutError)
75
+ def write(*msg, timeout: nil, exception: nil)
76
+ NotConnected.raise! if closed?
77
+ timeout.nil? && @deadline and
78
+ return write_with_deadline(msg, @deadline, exception)
79
+ timeout = (timeout || @cfg.write_timeout).to_f
80
+ return @socket.write(*msg) unless timeout.positive?
81
+ write_with_deadline(msg, Time.now + timeout, exception)
83
82
  end
84
83
 
85
84
  def flush
86
85
  @socket.flush unless closed?
87
86
  self
88
87
  end
88
+
89
+ private
90
+
91
+ def read_with_deadline(nbytes, deadline, exception)
92
+ @socket.read_with_deadline(
93
+ nbytes,
94
+ deadline,
95
+ exception || @cfg.read_timeout_error
96
+ )
97
+ end
98
+
99
+ def write_with_deadline(msg, deadline, exception)
100
+ exception ||= @cfg.write_timeout_error
101
+ result = 0
102
+ msg.each do |chunk|
103
+ result += @socket.write_with_deadline(chunk.b, deadline, exception)
104
+ end
105
+ result
106
+ end
89
107
  end
@@ -26,7 +26,7 @@ class TCPClient
26
26
  end
27
27
 
28
28
  def to_h
29
- {host: @hostname, port: @addrinfo.ip_port}
29
+ { host: @hostname, port: @addrinfo.ip_port }
30
30
  end
31
31
 
32
32
  def ==(other)
@@ -1,3 +1,5 @@
1
+ require_relative 'errors'
2
+
1
3
  class TCPClient
2
4
  class Configuration
3
5
  def self.create(options = {})
@@ -6,15 +8,38 @@ class TCPClient
6
8
  ret
7
9
  end
8
10
 
9
- attr_reader :buffered, :keep_alive, :reverse_lookup, :timeout
11
+ attr_reader :buffered,
12
+ :keep_alive,
13
+ :reverse_lookup,
14
+ :timeout,
15
+ :connect_timeout,
16
+ :read_timeout,
17
+ :write_timeout,
18
+ :connect_timeout_error,
19
+ :read_timeout_error,
20
+ :write_timeout_error
10
21
  attr_accessor :ssl_params
11
22
 
12
23
  def initialize(options = {})
13
24
  @buffered = @keep_alive = @reverse_lookup = true
14
25
  self.timeout = @ssl_params = nil
26
+ @connect_timeout_error = ConnectTimeoutError
27
+ @read_timeout_error = ReadTimeoutError
28
+ @write_timeout_error = WriteTimeoutError
15
29
  options.each_pair { |attribute, value| set(attribute, value) }
16
30
  end
17
31
 
32
+ def freeze
33
+ @ssl_params.freeze
34
+ super
35
+ end
36
+
37
+ def initialize_copy(_org)
38
+ super
39
+ @ssl_params = @ssl_params.dup
40
+ self
41
+ end
42
+
18
43
  def ssl?
19
44
  @ssl_params ? true : false
20
45
  end
@@ -38,32 +63,41 @@ class TCPClient
38
63
  end
39
64
 
40
65
  def timeout=(seconds)
41
- @timeout = seconds(seconds)
42
- @connect_timeout = @write_timeout = @read_timeout = nil
43
- end
44
-
45
- def connect_timeout
46
- @connect_timeout || @timeout
66
+ @timeout =
67
+ @connect_timeout = @write_timeout = @read_timeout = seconds(seconds)
47
68
  end
48
69
 
49
70
  def connect_timeout=(seconds)
50
71
  @connect_timeout = seconds(seconds)
51
72
  end
52
73
 
53
- def write_timeout
54
- @write_timeout || @timeout
74
+ def read_timeout=(seconds)
75
+ @read_timeout = seconds(seconds)
55
76
  end
56
77
 
57
78
  def write_timeout=(seconds)
58
79
  @write_timeout = seconds(seconds)
59
80
  end
60
81
 
61
- def read_timeout
62
- @read_timeout || @timeout
82
+ def timeout_error=(exception)
83
+ NotAnException.raise!(exception) unless exception_class?(exception)
84
+ @connect_timeout_error =
85
+ @read_timeout_error = @write_timeout_error = exception
63
86
  end
64
87
 
65
- def read_timeout=(seconds)
66
- @read_timeout = seconds(seconds)
88
+ def connect_timeout_error=(exception)
89
+ NotAnException.raise!(exception) unless exception_class?(exception)
90
+ @connect_timeout_error = exception
91
+ end
92
+
93
+ def read_timeout_error=(exception)
94
+ NotAnException.raise!(exception) unless exception_class?(exception)
95
+ @read_timeout_error = exception
96
+ end
97
+
98
+ def write_timeout_error=(exception)
99
+ NotAnException.raise!(exception) unless exception_class?(exception)
100
+ @write_timeout_error = exception
67
101
  end
68
102
 
69
103
  def to_h
@@ -75,6 +109,9 @@ class TCPClient
75
109
  connect_timeout: @connect_timeout,
76
110
  read_timeout: @read_timeout,
77
111
  write_timeout: @write_timeout,
112
+ connect_timeout_error: @connect_timeout_error,
113
+ read_timeout_error: @read_timeout_error,
114
+ write_timeout_error: @write_timeout_error,
78
115
  ssl_params: @ssl_params
79
116
  }
80
117
  end
@@ -90,10 +127,14 @@ class TCPClient
90
127
 
91
128
  private
92
129
 
130
+ def exception_class?(value)
131
+ value.is_a?(Class) && value < Exception
132
+ end
133
+
93
134
  def set(attribute, value)
94
135
  public_send("#{attribute}=", value)
95
136
  rescue NoMethodError
96
- raise(ArgumentError, "unknown attribute - #{attribute}")
137
+ UnknownAttribute.raise!(attribute)
97
138
  end
98
139
 
99
140
  def seconds(value)
@@ -6,10 +6,8 @@ class TCPClient
6
6
  class << self
7
7
  attr_reader :default_configuration
8
8
 
9
- def configure(options = {})
10
- cfg = Configuration.new(options)
11
- yield(cfg) if block_given?
12
- @default_configuration = cfg
9
+ def configure(options = {}, &block)
10
+ @default_configuration = Configuration.create(options, &block)
13
11
  end
14
12
  end
15
13
 
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'socket'
4
+
5
+ class TCPClient
6
+ class NoOpenSSL < RuntimeError
7
+ def self.raise!
8
+ raise(self, 'OpenSSL is not avail', caller(1))
9
+ end
10
+ end
11
+
12
+ class NoBlockGiven < RuntimeError
13
+ def self.raise!
14
+ raise(self, 'no block given', caller(1))
15
+ end
16
+ end
17
+
18
+ class InvalidDeadLine < ArgumentError
19
+ def self.raise!(timeout)
20
+ raise(self, "invalid deadline - #{timeout}", caller(1))
21
+ end
22
+ end
23
+
24
+ class UnknownAttribute < ArgumentError
25
+ def self.raise!(attribute)
26
+ raise(self, "unknown attribute - #{attribute}", caller(1))
27
+ end
28
+ end
29
+
30
+ class NotAnException < TypeError
31
+ def self.raise!(object)
32
+ raise(self, "not a valid exception class - #{object.inspect}", caller(1))
33
+ end
34
+ end
35
+
36
+ class NotConnected < SocketError
37
+ def self.raise!
38
+ raise(self, 'client not connected', caller(1))
39
+ end
40
+ end
41
+
42
+ TimeoutError = Class.new(IOError)
43
+ ConnectTimeoutError = Class.new(TimeoutError)
44
+ ReadTimeoutError = Class.new(TimeoutError)
45
+ WriteTimeoutError = Class.new(TimeoutError)
46
+
47
+ Timeout = TimeoutError # backward compatibility
48
+ deprecate_constant(:Timeout)
49
+ end
@@ -1,3 +1,7 @@
1
+ # I keep this file for backward compatibility.
2
+ # This may be removed in one of the next versions.
3
+ # Let me know if you like to use it elsewhere...
4
+
1
5
  IOTimeoutError = Class.new(IOError) unless defined?(IOTimeoutError)
2
6
 
3
7
  module IOTimeoutMixin
@@ -10,6 +14,24 @@ module IOTimeoutMixin
10
14
  end
11
15
  end
12
16
 
17
+ def read_with_deadline(nbytes, deadline, exception)
18
+ result = ''.b
19
+ return result if nbytes.zero?
20
+ loop do
21
+ junk_size = nbytes - result.bytesize
22
+ read =
23
+ with_deadline(deadline, exception) do
24
+ read_nonblock(junk_size, exception: false)
25
+ end
26
+ unless read
27
+ close
28
+ return result
29
+ end
30
+ result += read
31
+ return result if result.bytesize >= nbytes
32
+ end
33
+ end
34
+
13
35
  def read(nbytes, timeout: nil, exception: IOTimeoutError)
14
36
  timeout = timeout.to_f
15
37
  return read_all(nbytes) { |junk_size| super(junk_size) } if timeout <= 0
@@ -35,8 +57,8 @@ module IOTimeoutMixin
35
57
  private
36
58
 
37
59
  def read_all(nbytes)
38
- return '' if nbytes.zero?
39
- result = ''
60
+ return ''.b if nbytes.zero?
61
+ result = ''.b
40
62
  loop do
41
63
  unless read = yield(nbytes - result.bytesize)
42
64
  close
@@ -0,0 +1,83 @@
1
+ module IOWithDeadlineMixin
2
+ def self.included(mod)
3
+ im = mod.instance_methods
4
+ if im.index(:wait_writable) && im.index(:wait_readable)
5
+ mod.include(ViaWaitMethod)
6
+ else
7
+ mod.include(ViaSelect)
8
+ end
9
+ end
10
+
11
+ def read_with_deadline(nbytes, deadline, exclass)
12
+ raise(exclass) if Time.now > deadline
13
+ result = ''.b
14
+ return result if nbytes.zero?
15
+ loop do
16
+ read =
17
+ with_deadline(deadline, exclass) do
18
+ read_nonblock(nbytes - result.bytesize, exception: false)
19
+ end
20
+ unless read
21
+ close
22
+ return result
23
+ end
24
+ result += read
25
+ return result if result.bytesize >= nbytes
26
+ end
27
+ end
28
+
29
+ def write_with_deadline(data, deadline, exclass)
30
+ raise(exclass) if Time.now > deadline
31
+ return 0 if (size = data.bytesize).zero?
32
+ result = 0
33
+ loop do
34
+ written =
35
+ with_deadline(deadline, exclass) do
36
+ write_nonblock(data, exception: false)
37
+ end
38
+ result += written
39
+ return result if result >= size
40
+ data = data.byteslice(written, data.bytesize - written)
41
+ end
42
+ end
43
+
44
+ module ViaWaitMethod
45
+ private def with_deadline(deadline, exclass)
46
+ loop do
47
+ case ret = yield
48
+ when :wait_writable
49
+ raise(exclass) if (remaining_time = deadline - Time.now) <= 0
50
+ raise(exclass) if wait_writable(remaining_time).nil?
51
+ when :wait_readable
52
+ raise(exclass) if (remaining_time = deadline - Time.now) <= 0
53
+ raise(exclass) if wait_readable(remaining_time).nil?
54
+ else
55
+ return ret
56
+ end
57
+ end
58
+ rescue Errno::ETIMEDOUT
59
+ raise(exclass)
60
+ end
61
+ end
62
+
63
+ module ViaSelect
64
+ private def with_deadline(deadline, exclass)
65
+ loop do
66
+ case ret = yield
67
+ when :wait_writable
68
+ raise(exclass) if (remaining_time = deadline - Time.now) <= 0
69
+ raise(exclass) if ::IO.select(nil, [self], nil, remaining_time).nil?
70
+ when :wait_readable
71
+ raise(exclass) if (remaining_time = deadline - Time.now) <= 0
72
+ raise(exclass) if ::IO.select([self], nil, nil, remaining_time).nil?
73
+ else
74
+ return ret
75
+ end
76
+ end
77
+ rescue Errno::ETIMEDOUT
78
+ raise(exclass)
79
+ end
80
+ end
81
+
82
+ private_constant(:ViaWaitMethod, :ViaSelect)
83
+ end
@@ -4,11 +4,11 @@ rescue LoadError
4
4
  return
5
5
  end
6
6
 
7
- require_relative 'mixin/io_timeout'
7
+ require_relative 'mixin/io_with_deadline'
8
8
 
9
9
  class TCPClient
10
10
  class SSLSocket < ::OpenSSL::SSL::SSLSocket
11
- include IOTimeoutMixin
11
+ include IOWithDeadlineMixin
12
12
 
13
13
  def initialize(socket, address, configuration, exception)
14
14
  ssl_params = Hash[configuration.ssl_params]
@@ -31,12 +31,13 @@ class TCPClient
31
31
 
32
32
  def connect_to(address, check, timeout, exception)
33
33
  self.hostname = address.hostname
34
- if timeout
34
+ timeout = timeout.to_f
35
+ if timeout.zero?
36
+ connect
37
+ else
35
38
  with_deadline(Time.now + timeout, exception) do
36
39
  connect_nonblock(exception: false)
37
40
  end
38
- else
39
- connect
40
41
  end
41
42
  post_connection_check(address.hostname) if check
42
43
  end
@@ -1,9 +1,9 @@
1
1
  require 'socket'
2
- require_relative 'mixin/io_timeout'
2
+ require_relative 'mixin/io_with_deadline'
3
3
 
4
4
  class TCPClient
5
5
  class TCPSocket < ::Socket
6
- include IOTimeoutMixin
6
+ include IOWithDeadlineMixin
7
7
 
8
8
  def initialize(address, configuration, exception)
9
9
  super(address.addrinfo.ipv6? ? :INET6 : :INET, :STREAM)
@@ -19,7 +19,8 @@ class TCPClient
19
19
  address.addrinfo.ip_port,
20
20
  address.addrinfo.ip_address
21
21
  )
22
- return connect(addr) unless timeout
22
+ timeout = timeout.to_f
23
+ return connect(addr) if timeout.zero?
23
24
  with_deadline(Time.now + timeout, exception) do
24
25
  connect_nonblock(addr, exception: false)
25
26
  end
@@ -1,3 +1,3 @@
1
1
  class TCPClient
2
- VERSION = '0.1.3'.freeze
2
+ VERSION = '0.2.3'.freeze
3
3
  end
data/rakefile.rb CHANGED
@@ -6,12 +6,12 @@ require 'bundler/gem_tasks'
6
6
 
7
7
  $stdout.sync = $stderr.sync = true
8
8
 
9
- task(:default) { exec('rake --tasks') }
10
-
11
9
  CLOBBER << 'prj'
12
10
 
13
- Rake::TestTask.new(:test) do |t|
14
- t.ruby_opts = %w[-w]
15
- t.verbose = true
16
- t.test_files = FileList['test/**/*_test.rb']
11
+ task(:default) { exec('rake --tasks') }
12
+
13
+ Rake::TestTask.new(:test) do |task|
14
+ task.test_files = FileList['test/**/*_test.rb']
15
+ task.ruby_opts = %w[-w]
16
+ task.verbose = true
17
17
  end
data/sample/google_ssl.rb CHANGED
@@ -2,20 +2,16 @@ require_relative '../lib/tcp-client'
2
2
 
3
3
  TCPClient.configure do |cfg|
4
4
  cfg.connect_timeout = 1 # second to connect the server
5
- cfg.write_timeout = 0.25 # seconds to write a single data junk
6
- cfg.read_timeout = 0.5 # seconds to read some bytes
7
5
  cfg.ssl_params = { ssl_version: :TLSv1_2 } # use TLS 1.2
8
6
  end
9
7
 
10
- # the following request sequence is not allowed to last longer than 2 seconds:
11
- # 1 second to connect (incl. SSL handshake etc.)
12
- # + 0.25 seconds to write data
13
- # + 0.5 seconds to read a response
14
-
15
8
  TCPClient.open('www.google.com:443') do |client|
16
- # simple HTTP get request
17
- pp client.write("GET / HTTP/1.1\r\nHost: google.com\r\n\r\n")
9
+ # query should not last longer than 0.5 seconds
10
+ client.with_deadline(0.5) do
11
+ # simple HTTP get request
12
+ pp client.write("GET / HTTP/1.1\r\nHost: google.com\r\n\r\n")
18
13
 
19
- # read "HTTP/1.1 " + 3 byte HTTP status code
20
- pp client.read(12)
14
+ # read "HTTP/1.1 " + 3 byte HTTP status code
15
+ pp client.read(12)
16
+ end
21
17
  end
@@ -13,7 +13,7 @@ class AddressTest < MiniTest::Test
13
13
  end
14
14
 
15
15
  def test_create_from_addrinfo
16
- addrinfo = Addrinfo.tcp('google.com', 42)
16
+ addrinfo = Addrinfo.tcp('localhost', 42)
17
17
  subject = TCPClient::Address.new(addrinfo)
18
18
  assert_equal(addrinfo.getnameinfo[0], subject.hostname)
19
19
  assert_equal(addrinfo, subject.addrinfo)
@@ -62,5 +62,4 @@ class AddressTest < MiniTest::Test
62
62
  assert(a == b)
63
63
  assert(a === b)
64
64
  end
65
-
66
65
  end
@@ -39,7 +39,8 @@ class ConfigurationTest < MiniTest::Test
39
39
  connect_timeout: 1,
40
40
  read_timeout: 2,
41
41
  write_timeout: 3,
42
- ssl: true
42
+ ssl: true,
43
+ connect_timeout_error: IOError
43
44
  )
44
45
  refute(subject.buffered)
45
46
  refute(subject.keep_alive)
@@ -48,6 +49,8 @@ class ConfigurationTest < MiniTest::Test
48
49
  assert_same(2, subject.read_timeout)
49
50
  assert_same(3, subject.write_timeout)
50
51
  assert(subject.ssl?)
52
+ assert_same(IOError, subject.connect_timeout_error)
53
+ assert_same(TCPClient::ReadTimeoutError, subject.read_timeout_error)
51
54
  end
52
55
 
53
56
  def test_invalid_option
@@ -88,6 +91,18 @@ class ConfigurationTest < MiniTest::Test
88
91
  assert_same(42, subject.write_timeout)
89
92
  end
90
93
 
94
+ def test_timeout_error_overwrite
95
+ subject = TCPClient::Configuration.new
96
+ assert_same(TCPClient::ConnectTimeoutError, subject.connect_timeout_error)
97
+ assert_same(TCPClient::ReadTimeoutError, subject.read_timeout_error)
98
+ assert_same(TCPClient::WriteTimeoutError, subject.write_timeout_error)
99
+
100
+ subject.timeout_error = IOError
101
+ assert_same(IOError, subject.connect_timeout_error)
102
+ assert_same(IOError, subject.read_timeout_error)
103
+ assert_same(IOError, subject.write_timeout_error)
104
+ end
105
+
91
106
  def test_compare
92
107
  a = TCPClient::Configuration.new
93
108
  b = TCPClient::Configuration.new
@@ -95,4 +110,32 @@ class ConfigurationTest < MiniTest::Test
95
110
  assert(a == b)
96
111
  assert(a === b)
97
112
  end
113
+
114
+ def test_dup
115
+ source =
116
+ TCPClient::Configuration.new(
117
+ buffered: false,
118
+ keep_alive: false,
119
+ reverse_lookup: false,
120
+ connect_timeout: 1,
121
+ read_timeout: 2,
122
+ write_timeout: 3,
123
+ ssl: {
124
+ ssl_version: :TLSv1_2
125
+ }
126
+ )
127
+ shadow = source.dup.freeze
128
+
129
+ # some changes
130
+ source.buffered = true
131
+ source.write_timeout = 5
132
+ source.ssl_params[:err] = true
133
+ source.timeout_error = IOError
134
+
135
+ refute_equal(source.__id__, shadow.__id__)
136
+ refute(shadow.buffered)
137
+ assert_equal(3, shadow.write_timeout)
138
+ assert_equal({ ssl_version: :TLSv1_2 }, shadow.ssl_params)
139
+ assert_same(TCPClient::ReadTimeoutError, shadow.read_timeout_error)
140
+ end
98
141
  end
@@ -3,6 +3,8 @@ require_relative 'test_helper'
3
3
  class TCPClientTest < MiniTest::Test
4
4
  parallelize_me!
5
5
 
6
+ HUGE_AMOUNT_OF_DATA = Array.new(2024, '?' * 1024).freeze
7
+
6
8
  attr_reader :config
7
9
 
8
10
  def setup
@@ -65,7 +67,7 @@ class TCPClientTest < MiniTest::Test
65
67
  start_time = Time.now
66
68
  subject.read(42, timeout: timeout)
67
69
  end
68
- assert_in_delta(timeout, Time.now - start_time, 0.02)
70
+ assert_in_delta(timeout, Time.now - start_time, 0.15)
69
71
  end
70
72
  end
71
73
 
@@ -81,27 +83,63 @@ class TCPClientTest < MiniTest::Test
81
83
  start_time = nil
82
84
  assert_raises(TCPClient::WriteTimeoutError) do
83
85
  start_time = Time.now
84
-
85
- # send 1MB to avoid any TCP stack buffering
86
- args = Array.new(2024, '?' * 1024)
87
- subject.write(*args, timeout: timeout)
86
+ subject.write(*HUGE_AMOUNT_OF_DATA, timeout: timeout)
88
87
  end
89
- assert_in_delta(timeout, Time.now - start_time, 0.02)
88
+ assert_in_delta(timeout, Time.now - start_time, 0.15)
90
89
  end
91
90
  end
92
91
 
93
92
  def test_write_timeout
94
- check_write_timeout(0.1)
93
+ check_write_timeout(0.01)
95
94
  check_write_timeout(0.25)
96
95
  end
97
96
 
97
+ def test_write_deadline
98
+ TCPClient.open('localhost:1234', config) do |subject|
99
+ refute(subject.closed?)
100
+ assert_raises(TCPClient::WriteTimeoutError) do
101
+ subject.with_deadline(0.25) do |*args|
102
+ assert_equal([subject], args)
103
+ loop { subject.write('some data here') }
104
+ end
105
+ end
106
+ end
107
+ end
108
+
109
+ def test_read_deadline
110
+ TCPClient.open('localhost:1234', config) do |subject|
111
+ refute(subject.closed?)
112
+ assert_raises(TCPClient::ReadTimeoutError) do
113
+ subject.with_deadline(0.25) do |*args|
114
+ assert_equal([subject], args)
115
+ loop { subject.read(0) }
116
+ end
117
+ end
118
+ end
119
+ end
120
+
121
+ def xtest_read_write_deadline
122
+ TCPClient.open('localhost:1234', config) do |subject|
123
+ refute(subject.closed?)
124
+ assert_raises(TCPClient::TimeoutError) do
125
+ subject.with_deadline(0.25) do |*args|
126
+ assert_equal([subject], args)
127
+ loop do
128
+ subject.write('HUGE_AMOUNT_OF_DATA')
129
+ subject.read(0)
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
135
+
98
136
  def check_connect_timeout(ssl_config)
99
137
  start_time = nil
100
138
  assert_raises(TCPClient::ConnectTimeoutError) do
101
139
  start_time = Time.now
102
140
  TCPClient.new.connect('localhost:1234', ssl_config)
103
141
  end
104
- assert_in_delta(ssl_config.connect_timeout, Time.now - start_time, 0.02)
142
+ assert_in_delta(ssl_config.connect_timeout, Time.now - start_time, 0.11)
105
143
  end
106
144
 
107
145
  def test_connect_ssl_timeout
data/test/test_helper.rb CHANGED
@@ -4,8 +4,6 @@ require 'minitest/autorun'
4
4
  require 'minitest/parallel'
5
5
  require_relative '../lib/tcp-client'
6
6
 
7
- $stdout.sync = $stderr.sync = true
8
-
9
7
  # this pseudo-server never reads or writes anything
10
- DummyServer = TCPServer.new('localhost', 1234)
11
- Minitest.after_run { DummyServer.close }
8
+ PseudoServer = TCPServer.new('localhost', 1234)
9
+ Minitest.after_run { PseudoServer.close }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tcp-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Blumtritt
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-02-12 00:00:00.000000000 Z
11
+ date: 2021-05-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -71,7 +71,9 @@ files:
71
71
  - lib/tcp-client/address.rb
72
72
  - lib/tcp-client/configuration.rb
73
73
  - lib/tcp-client/default_configuration.rb
74
+ - lib/tcp-client/errors.rb
74
75
  - lib/tcp-client/mixin/io_timeout.rb
76
+ - lib/tcp-client/mixin/io_with_deadline.rb
75
77
  - lib/tcp-client/ssl_socket.rb
76
78
  - lib/tcp-client/tcp_socket.rb
77
79
  - lib/tcp-client/version.rb
@@ -106,7 +108,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
106
108
  - !ruby/object:Gem::Version
107
109
  version: '0'
108
110
  requirements: []
109
- rubygems_version: 3.2.9
111
+ rubygems_version: 3.2.15
110
112
  signing_key:
111
113
  specification_version: 4
112
114
  summary: A TCP client implementation with working timeout support.