arpie 0.0.3 → 0.0.4
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.
- data/README +7 -4
- data/Rakefile +1 -1
- data/lib/arpie.rb +1 -0
- data/lib/arpie/client.rb +6 -5
- data/lib/arpie/protocol.rb +300 -207
- data/lib/arpie/proxy.rb +29 -18
- data/lib/arpie/server.rb +4 -4
- data/lib/arpie/xmlrpc.rb +108 -0
- data/spec/client_server_spec.rb +107 -0
- data/spec/protocol_merge_and_split_spec.rb +47 -0
- data/spec/protocol_spec.rb +152 -0
- data/spec/spec_helper.rb +58 -0
- data/tools/benchmark.rb +3 -2
- data/tools/protocol_benchmark.rb +11 -14
- metadata +10 -5
data/lib/arpie/proxy.rb
CHANGED
@@ -45,24 +45,34 @@ module Arpie
|
|
45
45
|
def _handle endpoint, message
|
46
46
|
if !@handler.respond_to?(message.meth.to_sym) ||
|
47
47
|
(@interface && !@interface.index(message.meth.to_sym))
|
48
|
-
|
48
|
+
endpoint.write_message NoMethodError.new("No such method: #{message.meth.inspect}")
|
49
|
+
return
|
49
50
|
end
|
50
51
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
@uuids.reject! {|uuid, (time, value)|
|
56
|
-
time < latest_timestamp
|
57
|
-
} if latest_timestamp
|
52
|
+
begin
|
53
|
+
# Prune old serials. This can probably be optimized, but works well enough for now.
|
54
|
+
if @uuid_tracking && message.uuid && message.uuid.is_a?(Numeric)
|
55
|
+
message.uuid &= 0xffffffffffffffff
|
58
56
|
|
59
|
-
|
60
|
-
|
57
|
+
timestamps = @uuids.values.map {|v| v[0] }.sort
|
58
|
+
latest_timestamp = timestamps[-@max_uuids]
|
59
|
+
@uuids.reject! {|uuid, (time, value)|
|
60
|
+
time < latest_timestamp
|
61
|
+
} if latest_timestamp
|
61
62
|
|
62
|
-
|
63
|
-
|
64
|
-
end
|
63
|
+
endpoint.write_message((@uuids[message.uuid] ||=
|
64
|
+
[Time.now, @handler.send(message.meth, *message.argv)])[1])
|
65
65
|
|
66
|
+
else
|
67
|
+
endpoint.write_message @handler.send(message.meth, *message.argv)
|
68
|
+
end
|
69
|
+
rescue IOError
|
70
|
+
raise
|
71
|
+
|
72
|
+
rescue Exception => e
|
73
|
+
endpoint.write_message RuntimeError.new("Internal Error")
|
74
|
+
raise
|
75
|
+
end
|
66
76
|
end
|
67
77
|
end
|
68
78
|
|
@@ -70,12 +80,13 @@ module Arpie
|
|
70
80
|
# method calls to the remote ProxyServer.
|
71
81
|
# Note that the methods of Client cannot be proxied.
|
72
82
|
class ProxyClient < RPCClient
|
83
|
+
attr_accessor :namespace
|
73
84
|
|
74
|
-
def initialize
|
75
|
-
super
|
76
|
-
@protocol, @namespace = protocol,
|
85
|
+
def initialize *protocols
|
86
|
+
super
|
87
|
+
@protocol, @namespace = protocol, ""
|
77
88
|
@uuid_generator = lambda {|client, method, argv|
|
78
|
-
UUID.
|
89
|
+
UUID.random_create.to_i
|
79
90
|
}
|
80
91
|
end
|
81
92
|
|
@@ -95,7 +106,7 @@ module Arpie
|
|
95
106
|
uuid = @uuid_generator ?
|
96
107
|
@uuid_generator.call(self, meth, argv) : nil
|
97
108
|
|
98
|
-
call =
|
109
|
+
call = Arpie::RPCall.new(@namespace, meth, argv, uuid)
|
99
110
|
ret = self.request(call)
|
100
111
|
case ret
|
101
112
|
when Exception
|
data/lib/arpie/server.rb
CHANGED
@@ -36,11 +36,11 @@ module Arpie
|
|
36
36
|
|
37
37
|
attr_reader :endpoints
|
38
38
|
|
39
|
-
# Create a new Server with the given
|
39
|
+
# Create a new Server with the given protocols.
|
40
40
|
# You will need to define a handler, and an acceptor
|
41
41
|
# before it becomes operational.
|
42
|
-
def initialize
|
43
|
-
@protocol =
|
42
|
+
def initialize *protocols
|
43
|
+
@protocol = Arpie::ProtocolChain.new(*protocols)
|
44
44
|
@endpoints = []
|
45
45
|
|
46
46
|
@on_connect = lambda {|server, endpoint| }
|
@@ -120,7 +120,7 @@ module Arpie
|
|
120
120
|
def _acceptor_thread
|
121
121
|
loop do
|
122
122
|
client = @acceptor.call(self)
|
123
|
-
c = @protocol.
|
123
|
+
c = @protocol.endpoint_class.new(@protocol.clone, client)
|
124
124
|
Thread.new { _read_thread(c) }
|
125
125
|
end
|
126
126
|
end
|
data/lib/arpie/xmlrpc.rb
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'xmlrpc/create'
|
2
|
+
require 'xmlrpc/parser'
|
3
|
+
require 'xmlrpc/config'
|
4
|
+
|
5
|
+
module Arpie
|
6
|
+
|
7
|
+
class XMLRPCProtocol < Protocol
|
8
|
+
private_class_method :new
|
9
|
+
|
10
|
+
attr_accessor :writer
|
11
|
+
attr_accessor :parser
|
12
|
+
|
13
|
+
def setup
|
14
|
+
@writer ||= XMLRPC::Create.new
|
15
|
+
@parser ||= XMLRPC::XMLParser::REXMLStreamParser.new
|
16
|
+
end
|
17
|
+
private :setup
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
# A XMLRPC Protocol based on rubys xmlrpc stdlib.
|
22
|
+
# This does not encode HTTP headers; usage together with
|
23
|
+
# a real webserver is advised.
|
24
|
+
class XMLRPCClientProtocol < XMLRPCProtocol
|
25
|
+
public_class_method :new
|
26
|
+
|
27
|
+
def to object
|
28
|
+
setup
|
29
|
+
raise ArgumentError, "Can only encode Arpie::RPCall" unless
|
30
|
+
object.is_a?(Arpie::RPCall)
|
31
|
+
|
32
|
+
@writer.methodCall((object.ns.nil? ? '' : object.ns + '.') + object.meth, *object.argv)
|
33
|
+
end
|
34
|
+
|
35
|
+
def from binary
|
36
|
+
setup
|
37
|
+
yield @parser.parseMethodResponse(binary)[1]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class XMLRPCServerProtocol < XMLRPCProtocol
|
42
|
+
public_class_method :new
|
43
|
+
|
44
|
+
def to object
|
45
|
+
setup
|
46
|
+
case object
|
47
|
+
when Exception
|
48
|
+
# TODO: wrap XMLFault
|
49
|
+
raise NotImplementedError, "Cannot encode exceptions"
|
50
|
+
|
51
|
+
else
|
52
|
+
@writer.methodResponse(true, object)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def from binary
|
57
|
+
setup
|
58
|
+
vv = @parser.parseMethodCall(binary)
|
59
|
+
ns, meth = vv[0].split('.')
|
60
|
+
meth.nil? and begin meth, ns = ns, nil end
|
61
|
+
yield RPCall.new(ns, meth, vv[1])
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# This simulates a very basic HTTP XMLRPC client/server.
|
66
|
+
# It is not recommended to use this with production code.
|
67
|
+
class HTTPTestProtocol < Protocol
|
68
|
+
CAN_SEPARATE_MESSAGES = true
|
69
|
+
|
70
|
+
private_class_method :new
|
71
|
+
|
72
|
+
def from binary
|
73
|
+
# Simply strip all HTTP headers.
|
74
|
+
binary && binary.size > 0 or incomplete!
|
75
|
+
header, body_and_rest = binary.split(/\r\n\r\n/, 2)
|
76
|
+
header && body_and_rest or incomplete!
|
77
|
+
|
78
|
+
header =~ /^\s*content-length:\s+(\d+)$\s*/xi or stream_error! "No content-length was provided."
|
79
|
+
content_length = $1.to_i
|
80
|
+
|
81
|
+
content_length <= 0
|
82
|
+
body_and_rest.size >= content_length or incomplete!
|
83
|
+
|
84
|
+
body = body_and_rest[0, content_length]
|
85
|
+
|
86
|
+
yield body
|
87
|
+
|
88
|
+
header.size + 4 + content_length
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
class HTTPClientTestProtocol < HTTPTestProtocol
|
93
|
+
public_class_method :new
|
94
|
+
|
95
|
+
def to object
|
96
|
+
"GET / HTTP/1.[01]\r\nContent-Length: #{object.size}\r\n\r\n" + object
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
|
101
|
+
class HTTPServerTestProtocol < HTTPTestProtocol
|
102
|
+
public_class_method :new
|
103
|
+
|
104
|
+
def to object
|
105
|
+
"HTTP/1.0 200 OK\r\nContent-Length: #{object.size}\r\n\r\n" + object
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'spec_helper')
|
2
|
+
|
3
|
+
require 'socket'
|
4
|
+
require 'timeout'
|
5
|
+
|
6
|
+
describe "ClientServer", :shared => true do
|
7
|
+
before(:all) do
|
8
|
+
@tcp_server = TCPServer.new('127.0.0.1', 56000)
|
9
|
+
end
|
10
|
+
|
11
|
+
before do
|
12
|
+
@handler_errors = 0
|
13
|
+
$evaluate_calls = 0
|
14
|
+
@client = Arpie::Client.new(Arpie::MarshalProtocol.new, Arpie::SizedProtocol.new)
|
15
|
+
@server = Arpie::Server.new(Arpie::MarshalProtocol.new, Arpie::SizedProtocol.new)
|
16
|
+
@client.connect do TCPSocket.new('127.0.0.1', 56000) end
|
17
|
+
@server.accept do @tcp_server.accept end
|
18
|
+
@server.on_handler_error do |s, e, p, x|
|
19
|
+
@handler_errors += 1
|
20
|
+
end
|
21
|
+
|
22
|
+
@server.handle do |s, e, m|
|
23
|
+
e.write_message m
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "ProxyClientServer", :shared => true do
|
29
|
+
before(:all) do
|
30
|
+
@tcp_server = TCPServer.new('127.0.0.1', 56001)
|
31
|
+
end
|
32
|
+
|
33
|
+
before do
|
34
|
+
@handler_errors = 0
|
35
|
+
$evaluate_calls = 0
|
36
|
+
@client = Arpie::ProxyClient.new(Arpie::MarshalProtocol.new, Arpie::SizedProtocol.new)
|
37
|
+
@server = Arpie::ProxyServer.new(Arpie::MarshalProtocol.new, Arpie::SizedProtocol.new)
|
38
|
+
@client.connect do TCPSocket.new('127.0.0.1', 56001) end
|
39
|
+
@server.accept do @tcp_server.accept end
|
40
|
+
@server.on_handler_error do |s, e, p, x|
|
41
|
+
@handler_errors += 1
|
42
|
+
end
|
43
|
+
|
44
|
+
@server.handle((Class.new do
|
45
|
+
def evaluate_calls
|
46
|
+
$evaluate_calls += 1
|
47
|
+
end
|
48
|
+
def raise_something
|
49
|
+
raise "test"
|
50
|
+
end
|
51
|
+
end).new)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe Arpie::Client do
|
56
|
+
it_should_behave_like "ClientServer"
|
57
|
+
end
|
58
|
+
|
59
|
+
describe Arpie::EventedClient do
|
60
|
+
it_should_behave_like "ClientServer"
|
61
|
+
|
62
|
+
before do
|
63
|
+
@client = Arpie::EventedClient.new(Arpie::MarshalProtocol.new, Arpie::SizedProtocol.new)
|
64
|
+
@handler_calls = 0
|
65
|
+
@queue = Queue.new
|
66
|
+
@client.handle do
|
67
|
+
@queue.push true
|
68
|
+
@handler_calls += 1
|
69
|
+
end
|
70
|
+
@client.connect do TCPSocket.new('127.0.0.1', 56000) end
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should not allow reading messages" do
|
74
|
+
@client.should_not respond_to :read_message
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should call the handler for incoming messages" do
|
78
|
+
@client.write_message "test"
|
79
|
+
lambda {Timeout.timeout(1.0) { @queue.pop } }.should_not raise_error TimeoutError
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
describe Arpie::Server do
|
84
|
+
it_should_behave_like "ClientServer"
|
85
|
+
end
|
86
|
+
|
87
|
+
describe "ProxyServer" do
|
88
|
+
it_should_behave_like "ProxyClientServer"
|
89
|
+
it "should raise handler errors to the client" do
|
90
|
+
lambda { @client.raise_something }.should(raise_error RuntimeError, "Internal Error")
|
91
|
+
@handler_errors.should == 1
|
92
|
+
end
|
93
|
+
|
94
|
+
it "should not re-evaluate for already-seen uuids" do
|
95
|
+
@client.evaluate_calls
|
96
|
+
@client.evaluate_calls
|
97
|
+
@client.uuid_generator do 100 end
|
98
|
+
@client.evaluate_calls
|
99
|
+
@client.evaluate_calls
|
100
|
+
$evaluate_calls.should == 3
|
101
|
+
end
|
102
|
+
|
103
|
+
it "should not call handler errors for missing methods" do
|
104
|
+
lambda { @client.missing }.should raise_error NoMethodError
|
105
|
+
$evaluate_calls.should == 0
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'spec_helper')
|
2
|
+
|
3
|
+
class Splitter < Arpie::Protocol
|
4
|
+
def from binary
|
5
|
+
yield binary.size
|
6
|
+
binary.each_char do |c|
|
7
|
+
yield c
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class Merger < Arpie::Protocol
|
13
|
+
def from binary
|
14
|
+
unless @expect
|
15
|
+
@expect = binary or incomplete!
|
16
|
+
gulp!
|
17
|
+
end
|
18
|
+
|
19
|
+
assemble! binary, :d, :size => @expect
|
20
|
+
end
|
21
|
+
|
22
|
+
def assemble binaries, token, meta
|
23
|
+
binaries.size >= meta[:size] or incomplete!
|
24
|
+
yield binaries.join('')
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "Merger::Splitter::Sized" do subject { [Splitter, Arpie::SizedProtocol] }
|
29
|
+
it_should_behave_like "ProtocolChainSetup"
|
30
|
+
|
31
|
+
it "should split messages correctly" do
|
32
|
+
chain_write(t = 'test')
|
33
|
+
chain_read.should == t.size
|
34
|
+
for i in 1..t.size do
|
35
|
+
chain_read.should == t[i-1, 1]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "Merger::Splitter::Sized" do subject { [Merger, Splitter, Arpie::SizedProtocol] }
|
41
|
+
it_should_behave_like "ProtocolChainSetup"
|
42
|
+
|
43
|
+
it "should assemble split messages correctly" do
|
44
|
+
chain_write(t = 'test')
|
45
|
+
chain_read.should == t
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'spec_helper')
|
2
|
+
|
3
|
+
describe "ProtocolChain", :shared => true do
|
4
|
+
it_should_behave_like "ProtocolChainSetup"
|
5
|
+
|
6
|
+
it "should convert without io correctly" do
|
7
|
+
v = @chain.to @testdata_a
|
8
|
+
@chain.from(v).should == [@testdata_a]
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should read written binary data correctly" do
|
12
|
+
chain_write(@testdata_a)
|
13
|
+
chain_read.should == @testdata_a
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should read multiple written messages correctly" do
|
17
|
+
write = []
|
18
|
+
for i in 0...4 do
|
19
|
+
write << (i % 2 == 0 ? @testdata_a : @testdata_b)
|
20
|
+
end
|
21
|
+
chain_write(*write)
|
22
|
+
|
23
|
+
for i in 0...4 do
|
24
|
+
chain_read.should == (i % 2 == 0 ? @testdata_a : @testdata_b)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should not clobber the buffer before the first read" do
|
29
|
+
chain_write(@testdata_a)
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should parse a io-written buffer correctly" do
|
33
|
+
write = []
|
34
|
+
for i in 0...4 do
|
35
|
+
write << (i % 2 == 0 ? @testdata_a : @testdata_b)
|
36
|
+
end
|
37
|
+
chain_write(*write)
|
38
|
+
|
39
|
+
@chain.from(@r.readpartial(4096)).should == [@testdata_a, @testdata_b, @testdata_a, @testdata_b]
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should read messages greater than MTU correctly" do
|
43
|
+
chain_write(message = (@testdata_a * (Arpie::MTU + 10)))
|
44
|
+
chain_read.should == message
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should not fail on interleaved io streams" do
|
48
|
+
r2, w2 = IO.pipe
|
49
|
+
chain_write(@testdata_a)
|
50
|
+
@chain.write_message(w2, @testdata_b)
|
51
|
+
w2.close
|
52
|
+
chain_read.should == @testdata_a
|
53
|
+
@chain.read_message(r2).should == @testdata_b
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "ObjectProtocolChain", :shared => true do
|
58
|
+
it_should_behave_like "ProtocolChain"
|
59
|
+
# Now, lets try some variations.
|
60
|
+
|
61
|
+
it "should read written objects correctly" do
|
62
|
+
chain_write(
|
63
|
+
a = [1, 2.4, false, true, nil, "string"],
|
64
|
+
b = {1 => "hi", 2 => "test", 3 => "bloh"}
|
65
|
+
)
|
66
|
+
|
67
|
+
chain_read.should == a
|
68
|
+
chain_read.should == b
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe "RPCProtocolChain", :shared => true do
|
73
|
+
it_should_behave_like "RPCProtocolChainSetup"
|
74
|
+
|
75
|
+
it "should send namespace-less RPC calls correctly encoded" do
|
76
|
+
call = Arpie::RPCall.new(nil, 'meth', [1, 2, 3])
|
77
|
+
@client.write_message(@w, call)
|
78
|
+
@w.close
|
79
|
+
@server.read_message(@r).should == call
|
80
|
+
end
|
81
|
+
|
82
|
+
it "should send namespaced RPC calls correctly encoded" do
|
83
|
+
call = Arpie::RPCall.new('ns', 'meth', [1, 2, 3])
|
84
|
+
@client.write_message(@w, call)
|
85
|
+
@w.close
|
86
|
+
@server.read_message(@r).should == call
|
87
|
+
end
|
88
|
+
|
89
|
+
it "should encode result values correctly" do
|
90
|
+
for r in inp = [1, 2.4, false, true, "string", {"1"=>"hash"}, [1,2,3]]
|
91
|
+
@server.write_message(@w, r)
|
92
|
+
end
|
93
|
+
@w.close
|
94
|
+
|
95
|
+
for r in inp
|
96
|
+
@client.read_message(@r).should == r
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Now, lets try some variations.
|
102
|
+
|
103
|
+
describe "Sized" do subject { [Arpie::SizedProtocol] }
|
104
|
+
it_should_behave_like "ProtocolChain"
|
105
|
+
end
|
106
|
+
|
107
|
+
describe "Sized::Sized" do subject { [Arpie::SizedProtocol, Arpie::SizedProtocol] }
|
108
|
+
it_should_behave_like "ProtocolChain"
|
109
|
+
end
|
110
|
+
|
111
|
+
describe "Sized::Marshal::Sized" do subject { [Arpie::SizedProtocol, Arpie::MarshalProtocol, Arpie::SizedProtocol] }
|
112
|
+
it_should_behave_like "ProtocolChain"
|
113
|
+
end
|
114
|
+
|
115
|
+
# Shellwords is a bit of a special case, because it only encodes arrays.
|
116
|
+
describe "Shellwords::Separator" do subject { [Arpie::ShellwordsProtocol, Arpie::SeparatorProtocol] }
|
117
|
+
it_should_behave_like "ProtocolChain"
|
118
|
+
before do
|
119
|
+
@testdata_a, @testdata_b = ['I am test', '1'], ['I am test', '2']
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
describe "HTTPTest" do subject { [Arpie::HTTPClientTestProtocol] }
|
124
|
+
it_should_behave_like "ProtocolChain"
|
125
|
+
end
|
126
|
+
|
127
|
+
describe "HTTPTest" do subject { [Arpie::HTTPServerTestProtocol] }
|
128
|
+
it_should_behave_like "ProtocolChain"
|
129
|
+
end
|
130
|
+
|
131
|
+
describe "YAML" do subject { [Arpie::YAMLProtocol] }
|
132
|
+
it_should_behave_like "ObjectProtocolChain"
|
133
|
+
end
|
134
|
+
|
135
|
+
describe "XMLRPC::Sized" do subject {
|
136
|
+
[
|
137
|
+
[Arpie::XMLRPCClientProtocol, Arpie::SizedProtocol],
|
138
|
+
[Arpie::XMLRPCServerProtocol, Arpie::SizedProtocol]
|
139
|
+
]
|
140
|
+
}
|
141
|
+
it_should_behave_like "RPCProtocolChain"
|
142
|
+
end
|
143
|
+
|
144
|
+
describe "XMLRPC::HTTPTest" do subject {
|
145
|
+
[
|
146
|
+
[Arpie::XMLRPCClientProtocol, Arpie::HTTPClientTestProtocol],
|
147
|
+
[Arpie::XMLRPCServerProtocol, Arpie::HTTPServerTestProtocol]
|
148
|
+
]
|
149
|
+
}
|
150
|
+
it_should_behave_like "RPCProtocolChain"
|
151
|
+
end
|
152
|
+
|