ione-rpc 1.0.0.pre0
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/.yardopts +5 -0
- data/README.md +267 -0
- data/lib/ione/rpc/client.rb +302 -0
- data/lib/ione/rpc/client_peer.rb +78 -0
- data/lib/ione/rpc/codec.rb +153 -0
- data/lib/ione/rpc/peer.rb +53 -0
- data/lib/ione/rpc/server.rb +101 -0
- data/lib/ione/rpc/version.rb +7 -0
- data/lib/ione/rpc.rb +15 -0
- data/spec/ione/rpc/client_peer_spec.rb +109 -0
- data/spec/ione/rpc/client_spec.rb +614 -0
- data/spec/ione/rpc/codec_spec.rb +153 -0
- data/spec/ione/rpc/peer_common.rb +137 -0
- data/spec/ione/rpc/server_spec.rb +214 -0
- data/spec/spec_helper.rb +23 -0
- metadata +80 -0
@@ -0,0 +1,153 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Ione
|
4
|
+
module Rpc
|
5
|
+
# Codecs are used to encode and decode the messages sent between the client
|
6
|
+
# and server. Codecs must be able to decode frames in a streaming fashion,
|
7
|
+
# i.e. frames that come in pieces.
|
8
|
+
#
|
9
|
+
# If you want to control how messages are framed you can implement your
|
10
|
+
# own codec from scratch by implementing {#encode} and {#decode}, but most
|
11
|
+
# of the time you should only need to implement {#encode_message} and
|
12
|
+
# {#decode_message} which take care of encoding and decoding the message,
|
13
|
+
# and leave the framing to the default implementation. If you decide to
|
14
|
+
# implement {#encode} and {#decode} you don't need to subclass this class.
|
15
|
+
#
|
16
|
+
# Codecs must be stateless.
|
17
|
+
#
|
18
|
+
# Codecs must also make sure that these (conceptual) invariants hold:
|
19
|
+
# `decode(encode(message, channel)) == [message, channel]` and
|
20
|
+
# `encode(decode(frame)) == frame` (the return values are not entirely
|
21
|
+
# compatible, but the concept should be clear).
|
22
|
+
class Codec
|
23
|
+
# Encodes a frame with a header that includes the frame size and channel,
|
24
|
+
# and the message as body.
|
25
|
+
#
|
26
|
+
# @param [Object] message the message to encode
|
27
|
+
# @param [Integer] channel the channel to encode into the frame
|
28
|
+
# @return [String] an encoded frame with the message and channel
|
29
|
+
def encode(message, channel)
|
30
|
+
data = encode_message(message)
|
31
|
+
[1, channel, data.bytesize, data.to_s].pack('ccNa*')
|
32
|
+
end
|
33
|
+
|
34
|
+
# Decodes a frame, piece by piece if necessary.
|
35
|
+
#
|
36
|
+
# Since the IO layer has no knowledge about the framing it can't know
|
37
|
+
# how many bytes are needed before a frame can be fully decoded so instead
|
38
|
+
# the codec needs to be able to process partial frames. At the same time
|
39
|
+
# a codec can be used concurrently and must be stateless. To support partial
|
40
|
+
# decoding a codec can return a state instead of a decoded message until
|
41
|
+
# a complete frame can be decoded.
|
42
|
+
#
|
43
|
+
# The codec must return three values: the last value must be true if a
|
44
|
+
# if a message was decoded fully, and false if not enough bytes were
|
45
|
+
# available. When it is true the first piece is the message and the second
|
46
|
+
# the channel. When it is false the first piece is the partial decoded
|
47
|
+
# state (this can be any object at all) and the second is nil. The partial
|
48
|
+
# decoded state will be passed in as the second argument on the next call.
|
49
|
+
#
|
50
|
+
# In other words: the first time {#decode} is called the second argument
|
51
|
+
# will be nil, but on subsequent calls, until the third return value is
|
52
|
+
# true, the second argument will be whatever was returned by the previous
|
53
|
+
# call.
|
54
|
+
#
|
55
|
+
# The buffer might contain more bytes than needed to decode a frame, and
|
56
|
+
# the implementation must not consume these. The implementation must
|
57
|
+
# consume all of the bytes of the current frame, but none of the bytes of
|
58
|
+
# the next frame.
|
59
|
+
#
|
60
|
+
# @param [Ione::ByteBuffer] buffer the byte buffer that contains the frame
|
61
|
+
# data. The byte buffer is owned by the caller and should only be read from.
|
62
|
+
# @param [Object, nil] state the first value returned from the previous
|
63
|
+
# call, unless the previous call resulted in a completely decoded frame,
|
64
|
+
# in which case it is nil
|
65
|
+
# @return [Array<Object, Integer, Boolean>] three values where the last is
|
66
|
+
# true when a frame could be completely decoded. When the last value is
|
67
|
+
# true the first value is the decoded message and the second the channel,
|
68
|
+
# but when the last value is false the first is the partial state (see
|
69
|
+
# the `state` parameter) and the second is nil.
|
70
|
+
def decode(buffer, state)
|
71
|
+
state ||= State.new(buffer)
|
72
|
+
if state.header_ready?
|
73
|
+
state.read_header
|
74
|
+
end
|
75
|
+
if state.body_ready?
|
76
|
+
return decode_message(state.read_body), state.channel, true
|
77
|
+
else
|
78
|
+
return state, nil, false
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# @!method encode_message(message)
|
83
|
+
#
|
84
|
+
# Encode an object to bytes that can be sent over the network.
|
85
|
+
#
|
86
|
+
# @param [Object] message the object to encode
|
87
|
+
# @return [String, Ione::ByteBuffer] an object that responds to `#to_s`
|
88
|
+
# and `#bytesize`, for example a `String` or a `Ione::ByteBuffer`
|
89
|
+
|
90
|
+
# @!method decode_message(str)
|
91
|
+
#
|
92
|
+
# Decode a string of bytes to an object.
|
93
|
+
#
|
94
|
+
# @param [String] str an encoded message, the same string that was produced
|
95
|
+
# by {#encode_message}
|
96
|
+
# @return [Object] the decoded message
|
97
|
+
|
98
|
+
# @private
|
99
|
+
class State
|
100
|
+
attr_reader :channel
|
101
|
+
|
102
|
+
def initialize(buffer)
|
103
|
+
@buffer = buffer
|
104
|
+
end
|
105
|
+
|
106
|
+
def read_header
|
107
|
+
n = @buffer.read_short
|
108
|
+
@version = n >> 8
|
109
|
+
@channel = n & 0xff
|
110
|
+
@length = @buffer.read_int
|
111
|
+
end
|
112
|
+
|
113
|
+
def read_body
|
114
|
+
@buffer.read(@length)
|
115
|
+
end
|
116
|
+
|
117
|
+
def header_ready?
|
118
|
+
@length.nil? && @buffer.size >= 6
|
119
|
+
end
|
120
|
+
|
121
|
+
def body_ready?
|
122
|
+
@length && @buffer.size >= @length
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# A codec that works with encoders like JSON, MessagePack, YAML and others
|
128
|
+
# that follow the informal Ruby standard of having a `#dump` method that
|
129
|
+
# encodes and a `#load` method that decodes.
|
130
|
+
#
|
131
|
+
# @example A codec that encodes messages as JSON
|
132
|
+
# codec = StandardCodec.new(JSON)
|
133
|
+
#
|
134
|
+
# @example A codec that encodes messages as MessagePack
|
135
|
+
# codec = StandardCodec.new(MessagePack)
|
136
|
+
class StandardCodec < Codec
|
137
|
+
# @param [#load, #dump] delegate
|
138
|
+
def initialize(delegate)
|
139
|
+
@delegate = delegate
|
140
|
+
end
|
141
|
+
|
142
|
+
# Uses the delegate's `#dump` to encode the message
|
143
|
+
def encode_message(message)
|
144
|
+
@delegate.dump(message)
|
145
|
+
end
|
146
|
+
|
147
|
+
# Uses the delegate's `#load` to decode the message
|
148
|
+
def decode_message(str)
|
149
|
+
@delegate.load(str)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Ione
|
4
|
+
module Rpc
|
5
|
+
# @private
|
6
|
+
class Peer
|
7
|
+
attr_reader :host, :port
|
8
|
+
|
9
|
+
def initialize(connection, codec)
|
10
|
+
@connection = connection
|
11
|
+
@connection.on_data(&method(:handle_data))
|
12
|
+
@connection.on_closed(&method(:handle_closed))
|
13
|
+
@host = @connection.host
|
14
|
+
@port = @connection.port
|
15
|
+
@codec = codec
|
16
|
+
@buffer = Ione::ByteBuffer.new
|
17
|
+
@closed_promise = Promise.new
|
18
|
+
@current_message = nil
|
19
|
+
end
|
20
|
+
|
21
|
+
def on_closed(&listener)
|
22
|
+
@closed_promise.future.on_value(&listener)
|
23
|
+
end
|
24
|
+
|
25
|
+
def close
|
26
|
+
@connection.close
|
27
|
+
end
|
28
|
+
|
29
|
+
protected
|
30
|
+
|
31
|
+
def handle_data(new_data)
|
32
|
+
@buffer << new_data
|
33
|
+
while true
|
34
|
+
@current_message, channel, complete = @codec.decode(@buffer, @current_message)
|
35
|
+
break unless complete
|
36
|
+
handle_message(@current_message, channel)
|
37
|
+
@current_message = nil
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def handle_message(message, channel)
|
42
|
+
end
|
43
|
+
|
44
|
+
def handle_closed(cause=nil)
|
45
|
+
@closed_promise.fulfill(cause)
|
46
|
+
end
|
47
|
+
|
48
|
+
def write_message(message, channel)
|
49
|
+
@connection.write(@codec.encode(message, channel))
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Ione
|
4
|
+
module Rpc
|
5
|
+
# This is the base class of server peers.
|
6
|
+
#
|
7
|
+
# To implement a server you need to create a subclass of this class and
|
8
|
+
# implement {#handle_request}. You can also optionally implement
|
9
|
+
# {#handle_connection} to do initialization when a new client connects.
|
10
|
+
class Server
|
11
|
+
attr_reader :port
|
12
|
+
|
13
|
+
def initialize(port, codec, options={})
|
14
|
+
@port = port
|
15
|
+
@codec = codec
|
16
|
+
@io_reactor = options[:io_reactor] || Io::IoReactor.new
|
17
|
+
@stop_reactor = !options[:io_reactor]
|
18
|
+
@queue_length = options[:queue_size] || 5
|
19
|
+
@bind_address = options[:bind_address] || '0.0.0.0'
|
20
|
+
@logger = options[:logger]
|
21
|
+
end
|
22
|
+
|
23
|
+
# Start listening for client connections. This also starts the IO reactor
|
24
|
+
# if it was not already started.
|
25
|
+
#
|
26
|
+
# The returned future resolves when the server is ready to accept
|
27
|
+
# connections, or fails if there is an error starting the server.
|
28
|
+
#
|
29
|
+
# @return [Ione::Future<Ione::Rpc::Server>] a future that resolves to the
|
30
|
+
# server when all hosts have been connected to.
|
31
|
+
def start
|
32
|
+
@io_reactor.start.flat_map { setup_server }.map(self)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Stop the server and close all connections. This also stops the IO reactor
|
36
|
+
# if it has not already stopped.
|
37
|
+
#
|
38
|
+
# @return [Ione::Future<Ione::Rpc::Server>] a future that resolves to the
|
39
|
+
# server when all connections have closed and the IO reactor has stopped.
|
40
|
+
def stop
|
41
|
+
@io_reactor.stop.map(self)
|
42
|
+
end
|
43
|
+
|
44
|
+
protected
|
45
|
+
|
46
|
+
# Override this method to do work when a new client connects.
|
47
|
+
#
|
48
|
+
# This method may be called concurrently.
|
49
|
+
#
|
50
|
+
# @return [nil] the return value of this method is ignored
|
51
|
+
def handle_connection(connection)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Override this method to handle requests.
|
55
|
+
#
|
56
|
+
# You must respond to all requests, otherwise the client will eventually
|
57
|
+
# use up all of its channels and not be able to send any more requests.
|
58
|
+
#
|
59
|
+
# This method may be called concurrently.
|
60
|
+
#
|
61
|
+
# @param [Object] message a (decoded) message from a client
|
62
|
+
# @param [#host, #port, #on_closed] connection the client connection that
|
63
|
+
# received the message
|
64
|
+
# @return [Ione::Future<Object>] a future that will resolve to the response.
|
65
|
+
def handle_request(message, connection)
|
66
|
+
Future.resolved
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def setup_server
|
72
|
+
@io_reactor.bind(@bind_address, @port, @queue_length) do |acceptor|
|
73
|
+
@logger.info('Server listening for connections on %s:%d' % [@bind_address, @port]) if @logger
|
74
|
+
acceptor.on_accept do |connection|
|
75
|
+
@logger.info('Connection from %s:%d accepted' % [connection.host, connection.port]) if @logger
|
76
|
+
peer = ServerPeer.new(connection, @codec, self)
|
77
|
+
peer.on_closed do
|
78
|
+
@logger.info('Connection from %s:%d closed' % [connection.host, connection.port]) if @logger
|
79
|
+
end
|
80
|
+
handle_connection(peer)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# @private
|
86
|
+
class ServerPeer < Peer
|
87
|
+
def initialize(connection, codec, server)
|
88
|
+
super(connection, codec)
|
89
|
+
@server = server
|
90
|
+
end
|
91
|
+
|
92
|
+
def handle_message(message, channel)
|
93
|
+
f = @server.handle_request(message, self)
|
94
|
+
f.on_value do |response|
|
95
|
+
write_message(response, channel)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
data/lib/ione/rpc.rb
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
require 'ione/rpc/peer_common'
|
5
|
+
|
6
|
+
|
7
|
+
module Ione
|
8
|
+
module Rpc
|
9
|
+
describe ClientPeer do
|
10
|
+
let! :peer do
|
11
|
+
RpcSpec::TestClientPeer.new(connection, codec, max_channels)
|
12
|
+
end
|
13
|
+
|
14
|
+
let :connection do
|
15
|
+
RpcSpec::FakeConnection.new
|
16
|
+
end
|
17
|
+
|
18
|
+
let :codec do
|
19
|
+
double(:codec)
|
20
|
+
end
|
21
|
+
|
22
|
+
let :max_channels do
|
23
|
+
16
|
24
|
+
end
|
25
|
+
|
26
|
+
before do
|
27
|
+
codec.stub(:decode) do |buffer, current_frame|
|
28
|
+
message = buffer.to_s.scan(/[\w\d]+@\d+/).flatten.first
|
29
|
+
if message
|
30
|
+
payload, channel = message.split('@')
|
31
|
+
buffer.discard(message.bytesize)
|
32
|
+
[double(:complete, payload: payload), channel.to_i(10), true]
|
33
|
+
else
|
34
|
+
[double(:partial), nil, false]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
codec.stub(:encode) do |message, channel|
|
38
|
+
'%s@%03d' % [message, channel]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
include_examples 'peers'
|
43
|
+
|
44
|
+
context 'when the connection closes' do
|
45
|
+
it 'fails all outstanding requests when closing' do
|
46
|
+
f1 = peer.send_message('hello')
|
47
|
+
f2 = peer.send_message('world')
|
48
|
+
connection.closed_listener.call
|
49
|
+
expect { f1.value }.to raise_error(Io::ConnectionClosedError)
|
50
|
+
expect { f2.value }.to raise_error(Io::ConnectionClosedError)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe '#send_message' do
|
55
|
+
it 'encodes and sends a request frame' do
|
56
|
+
peer.send_message('hello')
|
57
|
+
connection.written_bytes.should start_with('hello')
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'uses the next available channel' do
|
61
|
+
peer.send_message('hello')
|
62
|
+
peer.send_message('foo')
|
63
|
+
connection.data_listener.call('world@0')
|
64
|
+
peer.send_message('bar')
|
65
|
+
connection.written_bytes.should == 'hello@000foo@001bar@000'
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'queues requests when all channels are in use' do
|
69
|
+
(max_channels + 2).times { peer.send_message('foo') }
|
70
|
+
connection.written_bytes.bytesize.should == max_channels * 7
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'sends queued requests when channels become available' do
|
74
|
+
(max_channels + 2).times { |i| peer.send_message("foo#{i.to_s.rjust(3, '0')}") }
|
75
|
+
length_before = connection.written_bytes.bytesize
|
76
|
+
connection.data_listener.call('bar@003')
|
77
|
+
connection.written_bytes[length_before, 10].should == "foo#{max_channels.to_s.rjust(3, '0')}@003"
|
78
|
+
connection.data_listener.call('bar@003')
|
79
|
+
connection.written_bytes[length_before + 10, 10].should == "foo#{(max_channels + 1).to_s.rjust(3, '0')}@003"
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'returns a future that resolves to the response' do
|
83
|
+
f = peer.send_message('foo')
|
84
|
+
f.should_not be_resolved
|
85
|
+
connection.data_listener.call('bar@000')
|
86
|
+
f.value.payload.should == 'bar'
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
module RpcSpec
|
94
|
+
class TestClientPeer < Ione::Rpc::ClientPeer
|
95
|
+
attr_reader :messages
|
96
|
+
|
97
|
+
def initialize(*)
|
98
|
+
super
|
99
|
+
@messages = []
|
100
|
+
end
|
101
|
+
|
102
|
+
def handle_message(*pair)
|
103
|
+
@messages << pair
|
104
|
+
super
|
105
|
+
end
|
106
|
+
|
107
|
+
public :send_message
|
108
|
+
end
|
109
|
+
end
|