jrpc 1.1.8 → 2.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 +4 -4
- data/.github/workflows/ci.yml +55 -0
- data/.rspec +1 -0
- data/.rubocop.yml +228 -0
- data/CHANGELOG.md +45 -0
- data/Gemfile +17 -0
- data/README.md +163 -13
- data/Rakefile +3 -1
- data/bin/console +1 -0
- data/bin/jrpc +37 -26
- data/bin/jrpc-shell +34 -24
- data/jrpc.gemspec +6 -8
- data/lib/jrpc/errors.rb +65 -0
- data/lib/jrpc/id_generator.rb +22 -0
- data/lib/jrpc/message.rb +78 -0
- data/lib/jrpc/shared_client/outbound_queue.rb +71 -0
- data/lib/jrpc/shared_client/registry.rb +46 -0
- data/lib/jrpc/shared_client/ticket.rb +84 -0
- data/lib/jrpc/shared_client/transport_loop.rb +290 -0
- data/lib/jrpc/shared_client.rb +194 -0
- data/lib/jrpc/simple_client.rb +89 -0
- data/lib/jrpc/transport/base.rb +60 -0
- data/lib/jrpc/transport/tcp.rb +243 -0
- data/lib/jrpc/transport.rb +12 -0
- data/lib/jrpc/version.rb +3 -1
- data/lib/jrpc.rb +13 -16
- metadata +23 -71
- data/.travis.yml +0 -4
- data/lib/jrpc/base_client.rb +0 -123
- data/lib/jrpc/error/client_error.rb +0 -5
- data/lib/jrpc/error/connection_error.rb +0 -11
- data/lib/jrpc/error/error.rb +0 -5
- data/lib/jrpc/error/internal_error.rb +0 -9
- data/lib/jrpc/error/internal_server_error.rb +0 -5
- data/lib/jrpc/error/invalid_params.rb +0 -9
- data/lib/jrpc/error/invalid_request.rb +0 -9
- data/lib/jrpc/error/method_not_found.rb +0 -9
- data/lib/jrpc/error/parse_error.rb +0 -9
- data/lib/jrpc/error/server_error.rb +0 -11
- data/lib/jrpc/error/unknown_error.rb +0 -5
- data/lib/jrpc/tcp_client.rb +0 -112
- data/lib/jrpc/transport/socket_base.rb +0 -88
- data/lib/jrpc/transport/socket_tcp.rb +0 -132
- data/lib/jrpc/utils.rb +0 -9
data/lib/jrpc/tcp_client.rb
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
require 'netstring'
|
|
2
|
-
require 'logger'
|
|
3
|
-
require 'benchmark'
|
|
4
|
-
module JRPC
|
|
5
|
-
class TcpClient < BaseClient
|
|
6
|
-
attr_reader :namespace, :transport
|
|
7
|
-
attr_accessor :logger
|
|
8
|
-
def_delegators :@transport, :close, :closed?, :connect
|
|
9
|
-
|
|
10
|
-
MAX_LOGGED_MESSAGE_LENGTH = 255
|
|
11
|
-
|
|
12
|
-
def initialize(uri, options = {})
|
|
13
|
-
super
|
|
14
|
-
@logger = @options.delete(:logger) || Logger.new($null)
|
|
15
|
-
@namespace = @options.delete(:namespace).to_s
|
|
16
|
-
|
|
17
|
-
timeout = @options.fetch(:timeout, 5)
|
|
18
|
-
connect_timeout = @options.fetch(:connect_timeout, timeout)
|
|
19
|
-
read_timeout = @options.fetch(:read_timeout, timeout)
|
|
20
|
-
write_timeout = @options.fetch(:write_timeout, 60) # default 60
|
|
21
|
-
connect_retry_count = @options.fetch(:connect_retry_count, 10) # default 10
|
|
22
|
-
@close_after_sent = @options.fetch(:close_after_sent, false)
|
|
23
|
-
|
|
24
|
-
@transport = JRPC::Transport::SocketTcp.new server: @uri,
|
|
25
|
-
connect_retry_count: connect_retry_count,
|
|
26
|
-
connect_timeout: connect_timeout,
|
|
27
|
-
read_timeout: read_timeout,
|
|
28
|
-
write_timeout: write_timeout
|
|
29
|
-
begin
|
|
30
|
-
@transport.connect
|
|
31
|
-
rescue JRPC::Transport::SocketTcp::Error
|
|
32
|
-
raise ConnectionError, "Can't connect to #{@uri}"
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
private
|
|
37
|
-
|
|
38
|
-
def ensure_connected
|
|
39
|
-
if @transport.closed?
|
|
40
|
-
logger.debug { 'Connecting transport...' }
|
|
41
|
-
@transport.connect
|
|
42
|
-
logger.debug { 'Connected.' }
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def send_command(request, options = {})
|
|
47
|
-
ensure_connected
|
|
48
|
-
read_timeout = options.fetch(:read_timeout)
|
|
49
|
-
write_timeout = options.fetch(:write_timeout)
|
|
50
|
-
response = nil
|
|
51
|
-
t = Benchmark.realtime do
|
|
52
|
-
logger.debug { "Request address: #{uri}" }
|
|
53
|
-
logger.debug { "Request message: #{Utils.truncate(request, MAX_LOGGED_MESSAGE_LENGTH)}" }
|
|
54
|
-
logger.debug { "Request read_timeout: #{read_timeout}" }
|
|
55
|
-
logger.debug { "Request write_timeout: #{write_timeout}" }
|
|
56
|
-
send_request(request, write_timeout)
|
|
57
|
-
response = receive_response(read_timeout)
|
|
58
|
-
end
|
|
59
|
-
logger.debug do
|
|
60
|
-
"(#{'%.2f' % (t * 1000)}ms) Response message: #{Utils.truncate(response, MAX_LOGGED_MESSAGE_LENGTH)}"
|
|
61
|
-
end
|
|
62
|
-
response
|
|
63
|
-
ensure
|
|
64
|
-
@transport.close if @close_after_sent
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def send_notification(request, options = {})
|
|
68
|
-
ensure_connected
|
|
69
|
-
write_timeout = options.fetch(:write_timeout)
|
|
70
|
-
logger.debug { "Request address: #{uri}" }
|
|
71
|
-
logger.debug { "Request message: #{Utils.truncate(request, MAX_LOGGED_MESSAGE_LENGTH)}" }
|
|
72
|
-
logger.debug { "Request write_timeout: #{write_timeout}" }
|
|
73
|
-
send_request(request, write_timeout)
|
|
74
|
-
logger.debug { 'No response required' }
|
|
75
|
-
ensure
|
|
76
|
-
@transport.close if @close_after_sent
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def create_message(method, params)
|
|
80
|
-
super("#{namespace}#{method}", params)
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def send_request(request, timeout)
|
|
84
|
-
timeout ||= @transport.write_timeout
|
|
85
|
-
@transport.write Netstring.dump(request.to_s), timeout
|
|
86
|
-
rescue ::SocketError
|
|
87
|
-
raise ConnectionError, "Can't send request to #{uri}"
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def receive_response(timeout)
|
|
91
|
-
timeout ||= @transport.read_timeout
|
|
92
|
-
length = get_msg_length(timeout)
|
|
93
|
-
response = @transport.read(length + 1, timeout)
|
|
94
|
-
raise ClientError.new('invalid response. missed comma as terminator') if response[-1] != ','
|
|
95
|
-
response.chomp(',')
|
|
96
|
-
rescue ::SocketError
|
|
97
|
-
raise ConnectionError, "Can't receive response from #{uri}"
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def get_msg_length(timeout)
|
|
101
|
-
length = ''
|
|
102
|
-
while true do
|
|
103
|
-
character = @transport.read(1, timeout)
|
|
104
|
-
break if character == ':'
|
|
105
|
-
length += character
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
Integer(length)
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
end
|
|
112
|
-
end
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
module JRPC
|
|
2
|
-
module Transport
|
|
3
|
-
class SocketBase
|
|
4
|
-
|
|
5
|
-
class Error < ::JRPC::Error
|
|
6
|
-
end
|
|
7
|
-
|
|
8
|
-
class TimeoutError < Error
|
|
9
|
-
def initialize
|
|
10
|
-
super(self.class.to_s.split('::').last)
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
class ReadTimeoutError < TimeoutError
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
class WriteTimeoutError < TimeoutError
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
class ConnectionTimeoutError < TimeoutError
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
class ConnectionFailedError < Error
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
class WriteFailedError < Error
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
class ReadFailedError < Error
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
attr_reader :options, :read_timeout, :write_timeout
|
|
33
|
-
|
|
34
|
-
def self.connect(options)
|
|
35
|
-
connection = new(options)
|
|
36
|
-
yield(connection)
|
|
37
|
-
ensure
|
|
38
|
-
connection.close if connection
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def initialize(options)
|
|
42
|
-
@server = options.fetch(:server)
|
|
43
|
-
@read_timeout = options.fetch(:read_timeout, nil)
|
|
44
|
-
@write_timeout = options.fetch(:write_timeout, nil)
|
|
45
|
-
@connect_timeout = options.fetch(:connect_timeout, nil)
|
|
46
|
-
@connect_retry_count = options.fetch(:connect_retry_count, 0)
|
|
47
|
-
@options = options
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def connect
|
|
51
|
-
retries = @connect_retry_count
|
|
52
|
-
|
|
53
|
-
while retries >= 0
|
|
54
|
-
begin
|
|
55
|
-
connect_socket
|
|
56
|
-
break
|
|
57
|
-
rescue Error => e
|
|
58
|
-
retries -= 1
|
|
59
|
-
raise e if retries < 0
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def read(_length, _timeout = @read_timeout)
|
|
65
|
-
raise NotImplementedError
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def write(_data, _timeout = @write_timeout)
|
|
69
|
-
raise NotImplementedError
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def close
|
|
73
|
-
raise NotImplementedError
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def closed?
|
|
77
|
-
raise NotImplementedError
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
private
|
|
81
|
-
|
|
82
|
-
def connect_socket
|
|
83
|
-
raise NotImplementedError
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
end
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
module JRPC
|
|
2
|
-
module Transport
|
|
3
|
-
class SocketTcp < SocketBase
|
|
4
|
-
|
|
5
|
-
def read(length, timeout = @read_timeout)
|
|
6
|
-
received = ''
|
|
7
|
-
length_to_read = length
|
|
8
|
-
while length_to_read > 0
|
|
9
|
-
io_read, = IO.select([socket], [], [], timeout)
|
|
10
|
-
raise ReadTimeoutError unless io_read
|
|
11
|
-
check_fin_signal
|
|
12
|
-
chunk = io_read[0].read_nonblock(length_to_read)
|
|
13
|
-
received += chunk
|
|
14
|
-
length_to_read -= chunk.bytesize
|
|
15
|
-
end
|
|
16
|
-
received
|
|
17
|
-
rescue Errno::EPIPE, EOFError => e
|
|
18
|
-
# EPIPE, in this case, means that the data connection was unexpectedly terminated.
|
|
19
|
-
clear_socket!
|
|
20
|
-
raise ReadFailedError, "#{e.class} #{e.message}"
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def write(data, timeout = @write_timeout)
|
|
24
|
-
length_written = 0
|
|
25
|
-
data_to_write = data
|
|
26
|
-
while data_to_write.bytesize > 0
|
|
27
|
-
_, io_write, = IO.select([], [socket], [], timeout)
|
|
28
|
-
raise WriteTimeoutError unless io_write
|
|
29
|
-
check_fin_signal
|
|
30
|
-
chunk_length = io_write[0].write_nonblock(data_to_write)
|
|
31
|
-
length_written += chunk_length
|
|
32
|
-
data_to_write = data.byteslice(length_written, data.length)
|
|
33
|
-
end
|
|
34
|
-
length_written
|
|
35
|
-
rescue Errno::EPIPE => e
|
|
36
|
-
# EPIPE, in this case, means that the data connection was unexpectedly terminated.
|
|
37
|
-
clear_socket!
|
|
38
|
-
raise WriteFailedError, "#{e.class} #{e.message}"
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def close
|
|
42
|
-
return if @socket.nil?
|
|
43
|
-
socket.close
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# Socket implementation allows client to send data to server after FIN,
|
|
47
|
-
# but server will never receive this data.
|
|
48
|
-
# So we consider socket closed when it have FIN event
|
|
49
|
-
# and close it correctly from client side.
|
|
50
|
-
def closed?
|
|
51
|
-
return true if @socket.nil? || socket.closed?
|
|
52
|
-
|
|
53
|
-
if fin_signal?
|
|
54
|
-
close
|
|
55
|
-
return true
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
false
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
# Socket implementation allows client to send data to server after FIN,
|
|
62
|
-
# but server will never receive this data.
|
|
63
|
-
# We correctly close socket from client side when FIN event received.
|
|
64
|
-
# Should be checked before send data to socket or recv data from socket.
|
|
65
|
-
def check_fin_signal
|
|
66
|
-
close if socket && !socket.closed? && fin_signal?
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def socket
|
|
70
|
-
@socket ||= build_socket
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
private
|
|
74
|
-
|
|
75
|
-
# when recv_nonblock(1) responds with empty string means that FIN event was received.
|
|
76
|
-
# in other cases it will return 1 byte string or raise EAGAINWaitReadable.
|
|
77
|
-
# MSG_PEEK means we do not move pointer when reading data.
|
|
78
|
-
# see https://apidock.com/ruby/BasicSocket/recv_nonblock
|
|
79
|
-
def fin_signal?
|
|
80
|
-
begin
|
|
81
|
-
resp = socket.recv_nonblock(1, Socket::MSG_PEEK)
|
|
82
|
-
rescue IO::EAGAINWaitReadable => _
|
|
83
|
-
resp = nil
|
|
84
|
-
end
|
|
85
|
-
resp == ''
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def clear_socket!
|
|
89
|
-
return if @socket.nil?
|
|
90
|
-
@socket.close unless @socket.closed?
|
|
91
|
-
@socket = nil
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def set_timeout_to(socket, type, value)
|
|
95
|
-
secs = Integer(value)
|
|
96
|
-
u_secs = Integer((value - secs) * 1_000_000)
|
|
97
|
-
opt_val = [secs, u_secs].pack('l_2')
|
|
98
|
-
socket.setsockopt Socket::SOL_SOCKET, type, opt_val
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def build_socket
|
|
102
|
-
host = @server.split(':').first
|
|
103
|
-
addr = Socket.getaddrinfo(host, nil)
|
|
104
|
-
Socket.new(Socket.const_get(addr[0][0]), Socket::SOCK_STREAM, 0)
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def connect_socket
|
|
108
|
-
host, port = @server.split(':')
|
|
109
|
-
addr = Socket.getaddrinfo(host, nil)
|
|
110
|
-
full_addr = Socket.pack_sockaddr_in(port, addr[0][3])
|
|
111
|
-
|
|
112
|
-
begin
|
|
113
|
-
socket.connect_nonblock(full_addr)
|
|
114
|
-
rescue IO::WaitWritable => _
|
|
115
|
-
if IO.select(nil, [socket], nil, @connect_timeout)
|
|
116
|
-
socket.connect_nonblock(full_addr)
|
|
117
|
-
else
|
|
118
|
-
clear_socket!
|
|
119
|
-
raise ConnectionFailedError, "Can't connect during #{@connect_timeout}"
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
rescue Errno::EISCONN => _
|
|
124
|
-
# already connected
|
|
125
|
-
rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ETIMEDOUT, Errno::EPIPE => e
|
|
126
|
-
clear_socket!
|
|
127
|
-
raise ConnectionFailedError, "#{e.class} #{e.message}"
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
end
|
|
131
|
-
end
|
|
132
|
-
end
|