marilyn-rpc 0.0.1
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/benchmark/client.rb +26 -0
- data/benchmark/server.rb +22 -0
- data/lib/marilyn-rpc.rb +4 -0
- data/lib/marilyn-rpc/client.rb +81 -0
- data/lib/marilyn-rpc/envelope.rb +62 -0
- data/lib/marilyn-rpc/gentleman.rb +64 -0
- data/lib/marilyn-rpc/mails.rb +101 -0
- data/lib/marilyn-rpc/server.rb +43 -0
- data/lib/marilyn-rpc/service.rb +15 -0
- data/lib/marilyn-rpc/service_cache.rb +47 -0
- data/lib/marilyn-rpc/version.rb +3 -0
- data/marilyn-rpc.gemspec +41 -0
- data/spec/envelope_spec.rb +70 -0
- data/spec/gentleman_spec.rb +57 -0
- data/spec/mails_spec.rb +69 -0
- data/spec/server_spec.rb +33 -0
- data/spec/service_cache.rb +45 -0
- data/spec/service_spec.rb +21 -0
- data/spec/spec_helper.rb +2 -0
- metadata +123 -0
data/benchmark/client.rb
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
$:.push('../lib')
|
|
2
|
+
require "marilyn-rpc"
|
|
3
|
+
client1 = MarilynRPC::NativeClient.connect_tcp('localhost', 8483)
|
|
4
|
+
TestService1 = client1.for(:test)
|
|
5
|
+
client2 = MarilynRPC::NativeClient.connect_unix("tmp.socket")
|
|
6
|
+
TestService2 = client2.for(:test)
|
|
7
|
+
|
|
8
|
+
require "benchmark"
|
|
9
|
+
n = 10000
|
|
10
|
+
Benchmark.bm(10) do |b|
|
|
11
|
+
b.report("tcp add") do
|
|
12
|
+
n.times { TestService1.add(1, 2) }
|
|
13
|
+
end
|
|
14
|
+
b.report("tcp time") do
|
|
15
|
+
n.times { TestService1.time.to_f }
|
|
16
|
+
end
|
|
17
|
+
b.report("unix add") do
|
|
18
|
+
n.times { TestService2.add(1, 2) }
|
|
19
|
+
end
|
|
20
|
+
b.report("unix time") do
|
|
21
|
+
n.times { TestService2.time.to_f }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
client1.disconnect
|
|
26
|
+
client2.disconnect
|
data/benchmark/server.rb
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require "rubygems"
|
|
2
|
+
$:.push('../lib')
|
|
3
|
+
require "marilyn-rpc"
|
|
4
|
+
require "eventmachine"
|
|
5
|
+
|
|
6
|
+
class TestService < MarilynRPC::Service
|
|
7
|
+
register :test
|
|
8
|
+
|
|
9
|
+
def time
|
|
10
|
+
Time.now
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def add(a, b)
|
|
14
|
+
a + b
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
EM.run {
|
|
20
|
+
EM.start_server "localhost", 8483, MarilynRPC::Server
|
|
21
|
+
EM.start_unix_domain_server("tmp.socket", MarilynRPC::Server)
|
|
22
|
+
}
|
data/lib/marilyn-rpc.rb
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
require 'socket'
|
|
2
|
+
|
|
3
|
+
module MarilynRPC
|
|
4
|
+
class NativeClientProxy
|
|
5
|
+
# Creates a new Native client proxy, were the calls get send to the remote
|
|
6
|
+
# side.
|
|
7
|
+
# @param [Object] path the path that is used to identify the service
|
|
8
|
+
# @param [Socekt] socket the socket to use for communication
|
|
9
|
+
def initialize(path, socket)
|
|
10
|
+
@path, @socket = path, socket
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Handler for calls to the remote system
|
|
14
|
+
def method_missing(method, *args, &block)
|
|
15
|
+
# since this client can't multiplex, we set the tag to nil
|
|
16
|
+
@socket.write(MarilynRPC::MailFactory.build_call(nil, @path, method, args))
|
|
17
|
+
|
|
18
|
+
# read the answer of the server back in
|
|
19
|
+
answer = MarilynRPC::Envelope.new
|
|
20
|
+
# read the header to have the size
|
|
21
|
+
answer.parse!(@socket.read(4))
|
|
22
|
+
# so now that we know the site, read the rest of the envelope
|
|
23
|
+
answer.parse!(@socket.read(answer.size))
|
|
24
|
+
|
|
25
|
+
# returns the result part of the mail or raise the exception if there is
|
|
26
|
+
# one
|
|
27
|
+
mail = MarilynRPC::MailFactory.unpack(answer)
|
|
28
|
+
if mail.is_a? MarilynRPC::CallResponseMail
|
|
29
|
+
mail.result
|
|
30
|
+
else
|
|
31
|
+
raise mail.exception
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The client that will handle the socket to the remote. The native client is
|
|
37
|
+
# written in pure ruby.
|
|
38
|
+
# @example Using the native client
|
|
39
|
+
# require "marilyn-rpc"
|
|
40
|
+
# client = MarilynRPC::NativeClient.connect_tcp('localhost', 8483)
|
|
41
|
+
# TestService = client.for(:test)
|
|
42
|
+
# TestService.add(1, 2)
|
|
43
|
+
# TestService.time.to_f
|
|
44
|
+
#
|
|
45
|
+
class NativeClient
|
|
46
|
+
# Create a native client for the socket.
|
|
47
|
+
# @param [Socket] socket the socket to manage
|
|
48
|
+
def initialize(socket)
|
|
49
|
+
@socket = socket
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Disconnect the client from the remote.
|
|
53
|
+
def disconnect
|
|
54
|
+
@socket.close
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Creates a new Proxy Object for the connection.
|
|
58
|
+
# @param [Object] path the path were the service is registered on the remote
|
|
59
|
+
# site
|
|
60
|
+
# @return [MarilynRPC::NativeClientProxy] the proxy obejct that will serve
|
|
61
|
+
# all calls
|
|
62
|
+
def for(path)
|
|
63
|
+
NativeClientProxy.new(path, @socket)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Connect to a unix domain socket.
|
|
67
|
+
# @param [String] path the path to the socket file.
|
|
68
|
+
# @return [MarilynRPC::NativeClient] the cónnected client
|
|
69
|
+
def self.connect_unix(path)
|
|
70
|
+
new(UNIXSocket.new(path))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Connect to a tcp socket.
|
|
74
|
+
# @param [String] host the host to cennect to (e.g. 'localhost')
|
|
75
|
+
# @param [Integer] port the port to connect to (e.g. 8000)
|
|
76
|
+
# @return [MarilynRPC::NativeClient] the cónnected client
|
|
77
|
+
def self.connect_tcp(host, port)
|
|
78
|
+
new(TCPSocket.open(host, port))
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# This class handles the envelope parsing and encoding which will be used by the
|
|
2
|
+
# server to handle multiple writes into the envelope.
|
|
3
|
+
class MarilynRPC::Envelope
|
|
4
|
+
# size of the envelope content
|
|
5
|
+
attr_reader :size
|
|
6
|
+
|
|
7
|
+
# create a new envelope instance
|
|
8
|
+
# @param [String] content the content of the new envelope
|
|
9
|
+
def initialize(content = nil)
|
|
10
|
+
self.content = content
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# parses the passed data
|
|
14
|
+
# @param [String] data parses the parsed data
|
|
15
|
+
# @return [String,nil] returns data that is not part of this string
|
|
16
|
+
# (in case the) parser gets more data than the length of the envelope.
|
|
17
|
+
# In case there are no data it will return nil.
|
|
18
|
+
def parse!(data)
|
|
19
|
+
@buffer += data
|
|
20
|
+
overhang = nil
|
|
21
|
+
|
|
22
|
+
# parse the length field of the
|
|
23
|
+
if @size.nil? && @buffer.size >= 4
|
|
24
|
+
# extract 4 bytes length header
|
|
25
|
+
@size = @buffer.slice!(0...4).unpack("N").first
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# envelope is complete and contains overhang
|
|
29
|
+
if !@size.nil? && @buffer.size > @size
|
|
30
|
+
overhang = @buffer.slice!(@size, @buffer.size)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
overhang
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# returns the content of the envelope
|
|
37
|
+
# @note should only be requested when the message if {finished?}.
|
|
38
|
+
# @return [String] the content of the envelope
|
|
39
|
+
def content
|
|
40
|
+
@buffer
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# sets the content of the envelope. If `nil` was passed an empty string will
|
|
44
|
+
# be set.
|
|
45
|
+
# @param [String] data the new content
|
|
46
|
+
def content=(content)
|
|
47
|
+
@buffer = content || ""
|
|
48
|
+
@size = content.nil? ? nil : content.size
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# encodes the envelope to be send over the wire
|
|
52
|
+
# @return [String] encoded envelope
|
|
53
|
+
def encode
|
|
54
|
+
[@size].pack("N") + @buffer
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# checks if the complete envelope was allready parsed
|
|
58
|
+
# @return [Boolean] `true` if the message was parsed
|
|
59
|
+
def finished?
|
|
60
|
+
@buffer.size == @size
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# The gentleman is a proxy onject that should help to create async responses on
|
|
2
|
+
# the server (service) side. There are two ways to use the gentleman. See the
|
|
3
|
+
# examples.
|
|
4
|
+
#
|
|
5
|
+
# @example Use the gentleman als passed block
|
|
6
|
+
# MarilynRPC::Gentleman.proxy do |helper|
|
|
7
|
+
# EM.system('ls', &helper)
|
|
8
|
+
#
|
|
9
|
+
# lambda do |output,status|
|
|
10
|
+
# output if status.exitstatus == 0
|
|
11
|
+
# end
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# @example Use the gentleman for responses that are objects
|
|
15
|
+
# conn = EM::Protocols::HttpClient2.connect 'google.com', 80
|
|
16
|
+
# req = conn.get('/')
|
|
17
|
+
# MarilynRPC::Gentleman.new(conn) do |response|
|
|
18
|
+
# response.content
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# The Gentleman has to be returned by the service method.
|
|
22
|
+
#
|
|
23
|
+
# @attr [Object] connection the connection where the response should be send
|
|
24
|
+
# @attr [Proc] callback the callback that will be called when the deferable
|
|
25
|
+
# was successful
|
|
26
|
+
# @attr [Object] tag the tag that should be used for the response
|
|
27
|
+
class MarilynRPC::Gentleman
|
|
28
|
+
attr_accessor :connection, :callback, :tag
|
|
29
|
+
|
|
30
|
+
# create a new proxy object using a deferable or the passed block.
|
|
31
|
+
# @param [EventMachine::Deferrable] deferable
|
|
32
|
+
def initialize(deferable = nil, &callback)
|
|
33
|
+
@callback = callback
|
|
34
|
+
if deferable
|
|
35
|
+
unless deferable.respond_to? :callback
|
|
36
|
+
raise ArgumentError.new("Wrong type, expected object that responds to #callback!")
|
|
37
|
+
end
|
|
38
|
+
gentleman = self
|
|
39
|
+
deferable.callback { |*args| gentleman.handle(*args) }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Creates a anonymous Gentleman proxy where the helper is exposed to, be able
|
|
44
|
+
# to use the Gentleman in situations where only a callback can be passed.
|
|
45
|
+
def self.proxy(&block)
|
|
46
|
+
gentleman = MarilynRPC::Gentleman.new
|
|
47
|
+
gentleman.callback = block.call(gentleman.helper)
|
|
48
|
+
gentleman
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The handler that will send the response to the remote system
|
|
52
|
+
# @param [Object] args the arguments that should be handled by the callback,
|
|
53
|
+
# the reponse of the callback will be send as result
|
|
54
|
+
def handle(*args)
|
|
55
|
+
mail = MarilynRPC::CallResponseMail.new(self.tag, self.callback.call(*args))
|
|
56
|
+
connection.send_mail(mail)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The helper that will be called by the deferable to call {handle} later
|
|
60
|
+
def helper
|
|
61
|
+
gentleman = self
|
|
62
|
+
lambda { |*args| gentleman.handle(*args) }
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
module MarilynRPC
|
|
2
|
+
# Helper that gets mixed into the mail classes to make common things easyer
|
|
3
|
+
module MailHelper
|
|
4
|
+
# generate a new serialize id which can be used in a mail
|
|
5
|
+
# @param [Integer] a number between 0 and 254
|
|
6
|
+
# @param [String] returns an 1 byte string as type id
|
|
7
|
+
def self.type(nr)
|
|
8
|
+
[nr].pack("c")
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# extracts the real data and ignores the type information
|
|
12
|
+
# @param [String] data the data to extract the mail from
|
|
13
|
+
# @return [String] the extracted data
|
|
14
|
+
def only_data(data)
|
|
15
|
+
data.slice(1, data.size)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# serialize the data using marilyns default serializer
|
|
19
|
+
# @param [Object] data the data to encode
|
|
20
|
+
# @param [String] the serialized data
|
|
21
|
+
def serialize(data)
|
|
22
|
+
Marshal.dump(data)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# deserializes the passed data to the original objects
|
|
26
|
+
# @param [String] data the serialized data
|
|
27
|
+
# @return [Object] the deserialized object
|
|
28
|
+
def deserialize(data)
|
|
29
|
+
Marshal.load(data)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class CallRequestMail < Struct.new(:tag, :path, :method, :args)
|
|
34
|
+
include MarilynRPC::MailHelper
|
|
35
|
+
TYPE = MarilynRPC::MailHelper.type(1)
|
|
36
|
+
|
|
37
|
+
def encode
|
|
38
|
+
TYPE + serialize([self.tag, self.path, self.method, self.args])
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def decode(data)
|
|
42
|
+
self.tag, self.path, self.method, self.args = deserialize(only_data(data))
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class CallResponseMail < Struct.new(:tag, :result)
|
|
47
|
+
include MarilynRPC::MailHelper
|
|
48
|
+
TYPE = MarilynRPC::MailHelper.type(2)
|
|
49
|
+
|
|
50
|
+
def encode
|
|
51
|
+
TYPE + serialize([self.tag, self.result])
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def decode(data)
|
|
55
|
+
self.tag, self.result = deserialize(only_data(data))
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
class ExceptionMail < Struct.new(:exception)
|
|
60
|
+
include MarilynRPC::MailHelper
|
|
61
|
+
TYPE = MarilynRPC::MailHelper.type(3)
|
|
62
|
+
|
|
63
|
+
def encode
|
|
64
|
+
TYPE + serialize(self.exception)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def decode(data)
|
|
68
|
+
self.exception = deserialize(only_data(data))
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Helper to destiguish between the different mails
|
|
73
|
+
module MailFactory
|
|
74
|
+
# Parses the envelop and generate the correct mail.
|
|
75
|
+
# @param [MarilynRPC::Envelope] envelope the envelope which contains a mail
|
|
76
|
+
# @return [MarilynRPC::CallRequestMail, MarilynRPC::CallResponseMail,
|
|
77
|
+
# MarilynRPC::ExceptionMail] the mail object that was extracted
|
|
78
|
+
def self.unpack(envelope)
|
|
79
|
+
data = envelope.content
|
|
80
|
+
type = data.slice(0, 1)
|
|
81
|
+
case type
|
|
82
|
+
when MarilynRPC::CallRequestMail::TYPE
|
|
83
|
+
mail = MarilynRPC::CallRequestMail.new
|
|
84
|
+
when MarilynRPC::CallResponseMail::TYPE
|
|
85
|
+
mail = MarilynRPC::CallResponseMail.new
|
|
86
|
+
when MarilynRPC::ExceptionMail::TYPE
|
|
87
|
+
mail = MarilynRPC::ExceptionMail.new
|
|
88
|
+
else
|
|
89
|
+
raise ArgumentError.new("The passed type #{type.inspect} is unknown!")
|
|
90
|
+
end
|
|
91
|
+
mail.decode(data)
|
|
92
|
+
mail
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# builds the binary data for a method call
|
|
96
|
+
def self.build_call(tag, path, method_name, args)
|
|
97
|
+
mail = MarilynRPC::CallRequestMail.new(tag, path, method_name, args)
|
|
98
|
+
MarilynRPC::Envelope.new(mail.encode).encode
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module MarilynRPC::Server
|
|
2
|
+
# Initialize the first recieving envelope for the connection and create the
|
|
3
|
+
# service cache since each connection gets it's own service instance.
|
|
4
|
+
def post_init
|
|
5
|
+
@envelope = MarilynRPC::Envelope.new
|
|
6
|
+
@cache = MarilynRPC::ServiceCache.new
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# Handler for the incoming data. EventMachine compatible.
|
|
10
|
+
# @param [String] data the data that should be parsed into envelopes
|
|
11
|
+
def receive_data(data)
|
|
12
|
+
overhang = @envelope.parse!(data)
|
|
13
|
+
|
|
14
|
+
# was massage parsed successfully?
|
|
15
|
+
if @envelope.finished?
|
|
16
|
+
begin
|
|
17
|
+
# grep the request
|
|
18
|
+
answer = @cache.call(MarilynRPC::MailFactory.unpack(@envelope))
|
|
19
|
+
if answer.is_a? MarilynRPC::CallResponseMail
|
|
20
|
+
send_mail(answer)
|
|
21
|
+
else
|
|
22
|
+
answer.connection = self # pass connection for async responses
|
|
23
|
+
end
|
|
24
|
+
rescue => exception
|
|
25
|
+
send_mail(MarilynRPC::ExceptionMail.new(exception))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# initialize the next envelope
|
|
29
|
+
@envelope = MarilynRPC::Envelope.new
|
|
30
|
+
receive_data(overhang) if overhang # reenter the data loop
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Send a response mail back on the wire of buffer
|
|
35
|
+
# @param [MarilynRPC::ExceptionMail, MarilynRPC::CallResponseMail] mail the
|
|
36
|
+
# mail that should be send to the client
|
|
37
|
+
def send_mail(mail)
|
|
38
|
+
send_data(MarilynRPC::Envelope.new(mail.encode).encode)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Handler for client disconnect
|
|
42
|
+
def unbind; end
|
|
43
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class MarilynRPC::Service
|
|
2
|
+
# registers the class where is was called as a service
|
|
3
|
+
# @param [String] path the path of the service
|
|
4
|
+
def self.register(path)
|
|
5
|
+
@@registry ||= {}
|
|
6
|
+
@@registry[path] = self
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# returns all services, that where registered
|
|
10
|
+
# @return [Hash<String, Object>] all registered services with path as key and
|
|
11
|
+
# the registered service as object
|
|
12
|
+
def self.registry
|
|
13
|
+
@@registry || {}
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
class MarilynRPC::ServiceCache
|
|
2
|
+
# creates the service cache
|
|
3
|
+
def initialize
|
|
4
|
+
@services = {}
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
# call a service in the service cache
|
|
8
|
+
# @param [MarilynRPC::CallRequestMail] mail the mail request object, that
|
|
9
|
+
# should be handled
|
|
10
|
+
# @return [MarilynRPC::CallResponseMail, MarilynRPC::Gentleman] either a
|
|
11
|
+
# Gentleman if the response is async or an direct response.
|
|
12
|
+
def call(mail)
|
|
13
|
+
# check if the correct mail object was send
|
|
14
|
+
unless mail.is_a?(MarilynRPC::CallRequestMail)
|
|
15
|
+
raise ArgumentError.new("Expected CallRequestMail Object!")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# call the service instance using the argument of the mail
|
|
19
|
+
# puts "call #{mail.method}@#{mail.path} with #{mail.args.inspect}"
|
|
20
|
+
result = lookup(mail.path).send(mail.method, *mail.args)
|
|
21
|
+
# puts "result => #{result.inspect}"
|
|
22
|
+
|
|
23
|
+
# no direct result, register callback
|
|
24
|
+
if result.is_a? MarilynRPC::Gentleman
|
|
25
|
+
result.tag = mail.tag # set the correct mail tag for the answer
|
|
26
|
+
result
|
|
27
|
+
else
|
|
28
|
+
# make response
|
|
29
|
+
MarilynRPC::CallResponseMail.new(mail.tag, result)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# get the service from the cache or the service registry
|
|
34
|
+
# @param [Object] path the path to the service (using the regestry)
|
|
35
|
+
# @return [Object] the service object or raises an ArgumentError
|
|
36
|
+
def lookup(path)
|
|
37
|
+
# lookup the service in the cache
|
|
38
|
+
if service = @services[path]
|
|
39
|
+
return service
|
|
40
|
+
# it's not in the cache, so try lookup in the service registry
|
|
41
|
+
elsif service = MarilynRPC::Service.registry[path]
|
|
42
|
+
return (@services[path] = service.new)
|
|
43
|
+
else
|
|
44
|
+
raise ArgumentError.new("Service #{mail.path} unknown!")
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
data/marilyn-rpc.gemspec
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
$:.push('lib')
|
|
3
|
+
require "marilyn-rpc/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |s|
|
|
6
|
+
s.name = "marilyn-rpc"
|
|
7
|
+
s.version = MarilynRPC::VERSION.dup
|
|
8
|
+
s.date = "2011-06-04"
|
|
9
|
+
s.summary = "Simple, beautiful event-based RPC"
|
|
10
|
+
s.email = "vilandgr+github@googlemail.com"
|
|
11
|
+
s.homepage = "https://github.com/threez/marilyn-rpc"
|
|
12
|
+
s.authors = ['Vincent Landgraf']
|
|
13
|
+
|
|
14
|
+
s.description = <<-EOF
|
|
15
|
+
A simple, beautiful event-based (EventMachine) RPC service and client library
|
|
16
|
+
EOF
|
|
17
|
+
|
|
18
|
+
dependencies = [
|
|
19
|
+
[:runtime, "eventmachine", "~> 0.12.10"],
|
|
20
|
+
[:development, "rspec", "~> 2.4"],
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
s.files = Dir['**/*']
|
|
24
|
+
s.test_files = Dir['test/**/*'] + Dir['spec/**/*']
|
|
25
|
+
s.executables = Dir['bin/*'].map { |f| File.basename(f) }
|
|
26
|
+
s.require_paths = ["lib"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
## Make sure you can build the gem on older versions of RubyGems too:
|
|
30
|
+
s.rubygems_version = "1.3.7"
|
|
31
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
|
32
|
+
s.specification_version = 3 if s.respond_to? :specification_version
|
|
33
|
+
|
|
34
|
+
dependencies.each do |type, name, version|
|
|
35
|
+
if s.respond_to?("add_#{type}_dependency")
|
|
36
|
+
s.send("add_#{type}_dependency", name, version)
|
|
37
|
+
else
|
|
38
|
+
s.add_dependency(name, version)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
|
2
|
+
|
|
3
|
+
describe MarilynRPC::Envelope do
|
|
4
|
+
before(:each) do
|
|
5
|
+
@envelope = MarilynRPC::Envelope.new
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
it "should be possible to parse really small envelope pieces" do
|
|
9
|
+
size = 100
|
|
10
|
+
content = "X" * size
|
|
11
|
+
data = [size].pack("N") + content
|
|
12
|
+
data.each_byte do |byte|
|
|
13
|
+
overhang = @envelope.parse!(byte.chr)
|
|
14
|
+
overhang.should == nil
|
|
15
|
+
end
|
|
16
|
+
@envelope.finished?.should == true
|
|
17
|
+
@envelope.content.should == content
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "should be possible to parse complete envelopes" do
|
|
21
|
+
size = 100
|
|
22
|
+
content = "X" * size
|
|
23
|
+
overhang = @envelope.parse!([size].pack("N") + content)
|
|
24
|
+
overhang.should == nil
|
|
25
|
+
@envelope.finished?.should == true
|
|
26
|
+
@envelope.content.should == content
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "shold be possible to parse an empty envelope" do
|
|
30
|
+
overhang = @envelope.parse!([0].pack("N"))
|
|
31
|
+
overhang.should == nil
|
|
32
|
+
@envelope.content.should == ""
|
|
33
|
+
@envelope.finished?.should == true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it "should be possible to detect overhangs correctly" do
|
|
37
|
+
content = "X" * 120
|
|
38
|
+
overhang = @envelope.parse!([100].pack("N") + content)
|
|
39
|
+
overhang.should == "X" * 20
|
|
40
|
+
@envelope.finished?.should == true
|
|
41
|
+
@envelope.content.should == "X" * 100
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "should be possible to lead an envelope from the overhang" do
|
|
45
|
+
content = ([20].pack("N") + ("X" * 20)) * 2
|
|
46
|
+
overhang = @envelope.parse!(content)
|
|
47
|
+
overhang.should == ([20].pack("N") + ("X" * 20))
|
|
48
|
+
@envelope.finished?.should == true
|
|
49
|
+
@envelope.content.should == "X" * 20
|
|
50
|
+
@envelope = MarilynRPC::Envelope.new
|
|
51
|
+
overhang = @envelope.parse!(overhang)
|
|
52
|
+
overhang.should == nil
|
|
53
|
+
@envelope.finished?.should == true
|
|
54
|
+
@envelope.content.should == "X" * 20
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it "should be possible to encode a envelope correctly" do
|
|
58
|
+
content = "Hallo Welt"
|
|
59
|
+
@envelope.content = content
|
|
60
|
+
@envelope.encode.should == [content.size].pack("N") + content
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it "should be possible to create a envelope using the initalizer" do
|
|
64
|
+
size = 100
|
|
65
|
+
content = "X" * size
|
|
66
|
+
@envelope = MarilynRPC::Envelope.new(content)
|
|
67
|
+
@envelope.finished?.should == true
|
|
68
|
+
@envelope.content.should == content
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
|
2
|
+
|
|
3
|
+
describe MarilynRPC::Gentleman do
|
|
4
|
+
before(:each) do
|
|
5
|
+
module MarilynRPC
|
|
6
|
+
def self.serialize(obj)
|
|
7
|
+
obj
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class DeferObjectMock
|
|
12
|
+
attr_accessor :callback
|
|
13
|
+
|
|
14
|
+
def callback(&block)
|
|
15
|
+
@callback = block
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call(*args)
|
|
19
|
+
@callback.call(*args)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class ConnectionMock
|
|
24
|
+
attr_accessor :data
|
|
25
|
+
def send_mail(obj)
|
|
26
|
+
@data = obj
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "should be possible to defer a process to a gentleman" do
|
|
32
|
+
deferable = DeferObjectMock.new
|
|
33
|
+
|
|
34
|
+
g = MarilynRPC::Gentleman.new(deferable) do |a, b|
|
|
35
|
+
a + b
|
|
36
|
+
end
|
|
37
|
+
g.connection = ConnectionMock.new
|
|
38
|
+
deferable.call(1, 2)
|
|
39
|
+
g.connection.data.result.should == 3
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "should be possible to create a gentleman helper" do
|
|
43
|
+
callback = nil
|
|
44
|
+
|
|
45
|
+
g = MarilynRPC::Gentleman.proxy do |helper|
|
|
46
|
+
callback = helper
|
|
47
|
+
|
|
48
|
+
lambda do |a, b|
|
|
49
|
+
a + b
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
g.connection = ConnectionMock.new
|
|
54
|
+
callback.call(1, 2)
|
|
55
|
+
g.connection.data.result.should == 3
|
|
56
|
+
end
|
|
57
|
+
end
|
data/spec/mails_spec.rb
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
|
2
|
+
|
|
3
|
+
describe "MarilynRPC Mails" do
|
|
4
|
+
describe MarilynRPC::CallRequestMail do
|
|
5
|
+
it "should be possible to serialize and deserialize a request" do
|
|
6
|
+
tag = Time.now.to_f
|
|
7
|
+
mail = MarilynRPC::CallRequestMail.new(
|
|
8
|
+
tag, "/user", :find_by_name, ["mr.x"])
|
|
9
|
+
data = mail.encode
|
|
10
|
+
data.should include("find_by_name")
|
|
11
|
+
mail = MarilynRPC::CallRequestMail.new()
|
|
12
|
+
mail.decode(data)
|
|
13
|
+
mail.tag.should == tag
|
|
14
|
+
mail.path.should == "/user"
|
|
15
|
+
mail.method.should == :find_by_name
|
|
16
|
+
mail.args.should == ["mr.x"]
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
describe MarilynRPC::CallResponseMail do
|
|
21
|
+
before(:each) do
|
|
22
|
+
class User < Struct.new(:name, :gid, :uid); end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "should be possible to serialize and deserialize a request" do
|
|
26
|
+
tag = Time.now.to_f
|
|
27
|
+
result = [
|
|
28
|
+
User.new("mr.x", 1, 1),
|
|
29
|
+
User.new("mr.xxx", 1, 2)
|
|
30
|
+
]
|
|
31
|
+
mail = MarilynRPC::CallResponseMail.new(tag, result)
|
|
32
|
+
data = mail.encode
|
|
33
|
+
data.should include("mr.xxx")
|
|
34
|
+
mail = MarilynRPC::CallResponseMail.new
|
|
35
|
+
mail.decode(data)
|
|
36
|
+
mail.tag.should == tag
|
|
37
|
+
mail.result.should == result
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
describe MarilynRPC::ExceptionMail do
|
|
42
|
+
it "should be possible to serialize and deserialize a request" do
|
|
43
|
+
exception = nil
|
|
44
|
+
begin
|
|
45
|
+
raise Exception.new "TestError"
|
|
46
|
+
rescue Exception => ex
|
|
47
|
+
exception = ex
|
|
48
|
+
end
|
|
49
|
+
mail = MarilynRPC::ExceptionMail.new(exception)
|
|
50
|
+
data = mail.encode
|
|
51
|
+
data.should include("TestError")
|
|
52
|
+
mail = MarilynRPC::ExceptionMail.new
|
|
53
|
+
mail.decode(data)
|
|
54
|
+
mail.exception.message.should == "TestError"
|
|
55
|
+
mail.exception.backtrace.size.should > 1
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
describe MarilynRPC::MailFactory do
|
|
60
|
+
it "it should be possible to unpack mails encapsulated in envelopes" do
|
|
61
|
+
tag = Time.now.to_f
|
|
62
|
+
mail = MarilynRPC::CallRequestMail.new(
|
|
63
|
+
tag, "/user", :find_by_name, ["mr.x"])
|
|
64
|
+
envelope = MarilynRPC::Envelope.new(mail.encode)
|
|
65
|
+
mail = MarilynRPC::MailFactory.unpack(envelope)
|
|
66
|
+
mail.should be_a(MarilynRPC::CallRequestMail)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
data/spec/server_spec.rb
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
|
2
|
+
|
|
3
|
+
describe MarilynRPC::Server do
|
|
4
|
+
before(:each) do
|
|
5
|
+
class ConnectionStub
|
|
6
|
+
include MarilynRPC::Server
|
|
7
|
+
attr_accessor :data
|
|
8
|
+
|
|
9
|
+
def initialize()
|
|
10
|
+
@data = ""
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def send_data(data)
|
|
14
|
+
@data += data
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
@server = ConnectionStub.new
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "should be possible to send multiple letters to the server" do
|
|
22
|
+
@server.post_init
|
|
23
|
+
@server.receive_data(MarilynRPC::Envelope.new("Test1").encode)
|
|
24
|
+
envelope = MarilynRPC::Envelope.new
|
|
25
|
+
envelope.parse!(@server.data)
|
|
26
|
+
mail = MarilynRPC::ExceptionMail.new
|
|
27
|
+
mail.decode(envelope.content)
|
|
28
|
+
mail.exception.message.should == "The passed type \"T\" is unknown!"
|
|
29
|
+
@server.receive_data(MarilynRPC::Envelope.new("Test2").encode)
|
|
30
|
+
@server.receive_data(MarilynRPC::Envelope.new("Test3").encode)
|
|
31
|
+
@server.unbind
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
|
2
|
+
|
|
3
|
+
describe MarilynRPC::ServiceCache do
|
|
4
|
+
before(:each) do
|
|
5
|
+
class DeferObjectMock
|
|
6
|
+
attr_accessor :callback
|
|
7
|
+
|
|
8
|
+
def callback(&block)
|
|
9
|
+
@callback = block
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def call(*args)
|
|
13
|
+
@callback.call(*args)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class TestService < MarilynRPC::Service
|
|
18
|
+
register "/test"
|
|
19
|
+
|
|
20
|
+
def sync_method(a, b)
|
|
21
|
+
a + b
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def async_method(a, b)
|
|
25
|
+
calc = DeferObjectMock.new
|
|
26
|
+
MarilynRPC::Gentleman.new(calc) { |result| result }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
@cache = MarilynRPC::ServiceCache.new
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "should be possible to call a sync method" do
|
|
33
|
+
mail = MarilynRPC::CallRequestMail.new(1, "/test", :sync_method, [1, 2])
|
|
34
|
+
answer = @cache.call(mail)
|
|
35
|
+
answer.should be_a(MarilynRPC::CallResponseMail)
|
|
36
|
+
answer.result.should == 3
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "should be possible to call an async method" do
|
|
40
|
+
mail = MarilynRPC::CallRequestMail.new(1, "/test", :async_method, [1, 2])
|
|
41
|
+
answer = @cache.call(mail)
|
|
42
|
+
answer.should be_a(MarilynRPC::Gentleman)
|
|
43
|
+
answer.tag.should == 1
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
|
2
|
+
|
|
3
|
+
describe MarilynRPC::Service do
|
|
4
|
+
before(:each) do
|
|
5
|
+
class TestService < MarilynRPC::Service
|
|
6
|
+
register "/test"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
class ExtendedTestService < MarilynRPC::Service
|
|
10
|
+
register "/test/extended"
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it "should have to registered services" do
|
|
15
|
+
MarilynRPC::Service.registry.size.should == 2
|
|
16
|
+
MarilynRPC::Service.registry.should == {
|
|
17
|
+
"/test/extended" => ExtendedTestService,
|
|
18
|
+
"/test" => TestService
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: marilyn-rpc
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
hash: 29
|
|
5
|
+
prerelease: false
|
|
6
|
+
segments:
|
|
7
|
+
- 0
|
|
8
|
+
- 0
|
|
9
|
+
- 1
|
|
10
|
+
version: 0.0.1
|
|
11
|
+
platform: ruby
|
|
12
|
+
authors:
|
|
13
|
+
- Vincent Landgraf
|
|
14
|
+
autorequire:
|
|
15
|
+
bindir: bin
|
|
16
|
+
cert_chain: []
|
|
17
|
+
|
|
18
|
+
date: 2011-06-04 00:00:00 +02:00
|
|
19
|
+
default_executable:
|
|
20
|
+
dependencies:
|
|
21
|
+
- !ruby/object:Gem::Dependency
|
|
22
|
+
name: eventmachine
|
|
23
|
+
prerelease: false
|
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
|
25
|
+
none: false
|
|
26
|
+
requirements:
|
|
27
|
+
- - ~>
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
hash: 59
|
|
30
|
+
segments:
|
|
31
|
+
- 0
|
|
32
|
+
- 12
|
|
33
|
+
- 10
|
|
34
|
+
version: 0.12.10
|
|
35
|
+
type: :runtime
|
|
36
|
+
version_requirements: *id001
|
|
37
|
+
- !ruby/object:Gem::Dependency
|
|
38
|
+
name: rspec
|
|
39
|
+
prerelease: false
|
|
40
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
|
41
|
+
none: false
|
|
42
|
+
requirements:
|
|
43
|
+
- - ~>
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
hash: 11
|
|
46
|
+
segments:
|
|
47
|
+
- 2
|
|
48
|
+
- 4
|
|
49
|
+
version: "2.4"
|
|
50
|
+
type: :development
|
|
51
|
+
version_requirements: *id002
|
|
52
|
+
description: |
|
|
53
|
+
A simple, beautiful event-based (EventMachine) RPC service and client library
|
|
54
|
+
|
|
55
|
+
email: vilandgr+github@googlemail.com
|
|
56
|
+
executables: []
|
|
57
|
+
|
|
58
|
+
extensions: []
|
|
59
|
+
|
|
60
|
+
extra_rdoc_files: []
|
|
61
|
+
|
|
62
|
+
files:
|
|
63
|
+
- benchmark/client.rb
|
|
64
|
+
- benchmark/server.rb
|
|
65
|
+
- lib/marilyn-rpc/client.rb
|
|
66
|
+
- lib/marilyn-rpc/envelope.rb
|
|
67
|
+
- lib/marilyn-rpc/gentleman.rb
|
|
68
|
+
- lib/marilyn-rpc/mails.rb
|
|
69
|
+
- lib/marilyn-rpc/server.rb
|
|
70
|
+
- lib/marilyn-rpc/service.rb
|
|
71
|
+
- lib/marilyn-rpc/service_cache.rb
|
|
72
|
+
- lib/marilyn-rpc/version.rb
|
|
73
|
+
- lib/marilyn-rpc.rb
|
|
74
|
+
- marilyn-rpc.gemspec
|
|
75
|
+
- spec/envelope_spec.rb
|
|
76
|
+
- spec/gentleman_spec.rb
|
|
77
|
+
- spec/mails_spec.rb
|
|
78
|
+
- spec/server_spec.rb
|
|
79
|
+
- spec/service_cache.rb
|
|
80
|
+
- spec/service_spec.rb
|
|
81
|
+
- spec/spec_helper.rb
|
|
82
|
+
has_rdoc: true
|
|
83
|
+
homepage: https://github.com/threez/marilyn-rpc
|
|
84
|
+
licenses: []
|
|
85
|
+
|
|
86
|
+
post_install_message:
|
|
87
|
+
rdoc_options: []
|
|
88
|
+
|
|
89
|
+
require_paths:
|
|
90
|
+
- lib
|
|
91
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
92
|
+
none: false
|
|
93
|
+
requirements:
|
|
94
|
+
- - ">="
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
hash: 3
|
|
97
|
+
segments:
|
|
98
|
+
- 0
|
|
99
|
+
version: "0"
|
|
100
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
101
|
+
none: false
|
|
102
|
+
requirements:
|
|
103
|
+
- - ">="
|
|
104
|
+
- !ruby/object:Gem::Version
|
|
105
|
+
hash: 3
|
|
106
|
+
segments:
|
|
107
|
+
- 0
|
|
108
|
+
version: "0"
|
|
109
|
+
requirements: []
|
|
110
|
+
|
|
111
|
+
rubyforge_project:
|
|
112
|
+
rubygems_version: 1.3.7
|
|
113
|
+
signing_key:
|
|
114
|
+
specification_version: 3
|
|
115
|
+
summary: Simple, beautiful event-based RPC
|
|
116
|
+
test_files:
|
|
117
|
+
- spec/envelope_spec.rb
|
|
118
|
+
- spec/gentleman_spec.rb
|
|
119
|
+
- spec/mails_spec.rb
|
|
120
|
+
- spec/server_spec.rb
|
|
121
|
+
- spec/service_cache.rb
|
|
122
|
+
- spec/service_spec.rb
|
|
123
|
+
- spec/spec_helper.rb
|