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.
@@ -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
@@ -0,0 +1,7 @@
1
+ # encoding: utf-8
2
+
3
+ module Ione
4
+ module Rpc
5
+ VERSION = '1.0.0.pre0'.freeze
6
+ end
7
+ end
data/lib/ione/rpc.rb ADDED
@@ -0,0 +1,15 @@
1
+ # encoding: utf-8
2
+
3
+ require 'ione'
4
+
5
+
6
+ module Ione
7
+ module Rpc
8
+ end
9
+ end
10
+
11
+ require 'ione/rpc/peer'
12
+ require 'ione/rpc/client_peer'
13
+ require 'ione/rpc/server'
14
+ require 'ione/rpc/client'
15
+ require 'ione/rpc/codec'
@@ -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