emrpc 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README +48 -0
- data/Rakefile +96 -0
- data/TODO +8 -0
- data/lib/emrpc/client.rb +28 -0
- data/lib/emrpc/em_connection.rb +19 -0
- data/lib/emrpc/fast_message_protocol.rb +111 -0
- data/lib/emrpc/marshal_protocol.rb +30 -0
- data/lib/emrpc/method_proxy.rb +38 -0
- data/lib/emrpc/multithreaded_client.rb +40 -0
- data/lib/emrpc/reconnecting_client.rb +46 -0
- data/lib/emrpc/server.rb +18 -0
- data/lib/emrpc/singlethreaded_client.rb +16 -0
- data/lib/emrpc/version.rb +3 -0
- data/lib/emrpc.rb +13 -0
- data/spec/fast_message_protocol_spec.rb +60 -0
- data/spec/marshal_protocol_spec.rb +50 -0
- data/spec/method_proxy_spec.rb +33 -0
- data/spec/multithreaded_client_spec.rb +49 -0
- data/spec/singlethreaded_client_spec.rb +23 -0
- data/spec/spec_helper.rb +64 -0
- data/spec/sync_client_spec.rb +39 -0
- metadata +104 -0
data/README
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
EMRPC is a EventMachine-based remote procedure call library.
|
2
|
+
It looks like DRb, but is much more efficient and provides
|
3
|
+
asynchronous interface along with blocking synchronous interface.
|
4
|
+
|
5
|
+
Author: Oleg Andreev <oleganza@gmail.com>
|
6
|
+
|
7
|
+
HELLO WORLD
|
8
|
+
|
9
|
+
class HelloWorld
|
10
|
+
def action
|
11
|
+
"Hello!"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
server = EMRPC::Server.new(:host => '0.0.0.0',
|
16
|
+
:port => 9000,
|
17
|
+
:object => HelloWorld.new)
|
18
|
+
|
19
|
+
EM::run do
|
20
|
+
server.run
|
21
|
+
end
|
22
|
+
|
23
|
+
client = EMRPC::Client.new(:host => "localhost", :port => 9000)
|
24
|
+
client.action == "Hello!" #=> true
|
25
|
+
|
26
|
+
|
27
|
+
THE MIT LICENSE
|
28
|
+
|
29
|
+
Copyright (c) 2008, Oleg Andreev <oleganza@gmail.com>
|
30
|
+
|
31
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
32
|
+
a copy of this software and associated documentation files (the
|
33
|
+
"Software"), to deal in the Software without restriction, including
|
34
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
35
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
36
|
+
permit persons to whom the Software is furnished to do so, subject to
|
37
|
+
the following conditions:
|
38
|
+
|
39
|
+
The above copyright notice and this permission notice shall be
|
40
|
+
included in all copies or substantial portions of the Software.
|
41
|
+
|
42
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
43
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
44
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
45
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
46
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
47
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
48
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
require "rake"
|
2
|
+
require "rake/clean"
|
3
|
+
require "rake/gempackagetask"
|
4
|
+
require "rake/rdoctask"
|
5
|
+
require "rake/testtask"
|
6
|
+
require "spec/rake/spectask"
|
7
|
+
require "fileutils"
|
8
|
+
|
9
|
+
def __DIR__
|
10
|
+
File.dirname(__FILE__)
|
11
|
+
end
|
12
|
+
|
13
|
+
include FileUtils
|
14
|
+
|
15
|
+
require "lib/emrpc/version"
|
16
|
+
|
17
|
+
def sudo
|
18
|
+
ENV['EMRPC_SUDO'] ||= "sudo"
|
19
|
+
sudo = windows? ? "" : ENV['EMRPC_SUDO']
|
20
|
+
end
|
21
|
+
|
22
|
+
def windows?
|
23
|
+
(PLATFORM =~ /win32|cygwin/) rescue nil
|
24
|
+
end
|
25
|
+
|
26
|
+
def install_home
|
27
|
+
ENV['GEM_HOME'] ? "-i #{ENV['GEM_HOME']}" : ""
|
28
|
+
end
|
29
|
+
|
30
|
+
##############################################################################
|
31
|
+
# Packaging & Installation
|
32
|
+
##############################################################################
|
33
|
+
CLEAN.include ["**/.*.sw?", "pkg", "lib/*.bundle", "*.gem", "doc/rdoc", ".config", "coverage", "cache"]
|
34
|
+
|
35
|
+
desc "Run the specs."
|
36
|
+
task :default => :specs
|
37
|
+
|
38
|
+
task :emrpc => [:clean, :rdoc, :package]
|
39
|
+
|
40
|
+
RUBY_FORGE_PROJECT = "emrpc"
|
41
|
+
PROJECT_URL = "http://strokedb.com"
|
42
|
+
PROJECT_SUMMARY = "Efficient RPC library with evented and blocking APIs. In all ways better than DRb."
|
43
|
+
PROJECT_DESCRIPTION = PROJECT_SUMMARY
|
44
|
+
|
45
|
+
AUTHOR = "Oleg Andreev"
|
46
|
+
EMAIL = "oleganza@gmail.com"
|
47
|
+
|
48
|
+
GEM_NAME = "emrpc"
|
49
|
+
PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
|
50
|
+
GEM_VERSION = EMRPC::VERSION + PKG_BUILD
|
51
|
+
|
52
|
+
RELEASE_NAME = "REL #{GEM_VERSION}"
|
53
|
+
|
54
|
+
require "extlib/tasks/release"
|
55
|
+
|
56
|
+
spec = Gem::Specification.new do |s|
|
57
|
+
s.name = GEM_NAME
|
58
|
+
s.version = GEM_VERSION
|
59
|
+
s.platform = Gem::Platform::RUBY
|
60
|
+
s.author = AUTHOR
|
61
|
+
s.email = EMAIL
|
62
|
+
s.homepage = PROJECT_URL
|
63
|
+
s.summary = PROJECT_SUMMARY
|
64
|
+
s.bindir = "bin"
|
65
|
+
s.description = s.summary
|
66
|
+
s.executables = %w( )
|
67
|
+
s.require_path = "lib"
|
68
|
+
s.files = %w( README Rakefile TODO ) + Dir["{docs,bin,spec,lib,examples,script}/**/*"]
|
69
|
+
|
70
|
+
# rdoc
|
71
|
+
s.has_rdoc = true
|
72
|
+
s.extra_rdoc_files = %w( README TODO )
|
73
|
+
#s.rdoc_options += RDOC_OPTS + ["--exclude", "^(app|uploads)"]
|
74
|
+
|
75
|
+
# Dependencies
|
76
|
+
s.add_dependency "eventmachine"
|
77
|
+
s.add_dependency "rake"
|
78
|
+
s.add_dependency "rspec"
|
79
|
+
# Requirements
|
80
|
+
s.requirements << "You need to install the json (or json_pure), yaml, rack gems to use related features."
|
81
|
+
s.required_ruby_version = ">= 1.8.4"
|
82
|
+
end
|
83
|
+
|
84
|
+
Rake::GemPackageTask.new(spec) do |package|
|
85
|
+
package.gem_spec = spec
|
86
|
+
end
|
87
|
+
|
88
|
+
desc "Run :package and install the resulting .gem"
|
89
|
+
task :install => :package do
|
90
|
+
sh %{#{sudo} gem install #{install_home} --local pkg/#{NAME}-#{EMRPC::VERSION}.gem --no-rdoc --no-ri}
|
91
|
+
end
|
92
|
+
|
93
|
+
desc "Run :clean and uninstall the .gem"
|
94
|
+
task :uninstall => :clean do
|
95
|
+
sh %{#{sudo} gem uninstall #{NAME}}
|
96
|
+
end
|
data/TODO
ADDED
data/lib/emrpc/client.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
module EMRPC
|
2
|
+
class Client
|
3
|
+
DEFAULT_TIMEOUT = 5 # 5 sec.
|
4
|
+
DEFAULT_PROTOCOL = :ClientProtocol # Default EventMachine connection protocol
|
5
|
+
DEFAULT_CONNECTIONS = 10 # 10 threads can operate concurrently, others will wait.
|
6
|
+
|
7
|
+
attr_reader :host, :port, :protocol, :timeout, :connections
|
8
|
+
# Create a regular object holding configuration,
|
9
|
+
# but returns a method proxy.
|
10
|
+
def self.new(*args, &blk)
|
11
|
+
client = super(*args)
|
12
|
+
backend = MultithreadedClient.new(:backends => client.connections,
|
13
|
+
:timeout => client.timeout)
|
14
|
+
MethodProxy.new(backend)
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(options = {})
|
18
|
+
@host = options[:host] or raise ":host required!"
|
19
|
+
@port = options[:port] or raise ":port required!"
|
20
|
+
@timeout = options[:timeout] || DEFAULT_TIMEOUT
|
21
|
+
@protocol = options[:protocol] || DEFAULT_PROTOCOL
|
22
|
+
@connections = Array.new(options.delete(:connections) || DEFAULT_CONNECTIONS) do
|
23
|
+
ClientConnection.new(options)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module EMRPC
|
2
|
+
module EmConnection
|
3
|
+
# Connection initialization hook,
|
4
|
+
def post_init
|
5
|
+
end
|
6
|
+
|
7
|
+
# Connection being safely established (post_init was already called).
|
8
|
+
def connection_completed
|
9
|
+
end
|
10
|
+
|
11
|
+
# Connection was closed.
|
12
|
+
def unbind
|
13
|
+
end
|
14
|
+
|
15
|
+
# Raw receive data callback.
|
16
|
+
def receive_data(data)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
module EMRPC
|
2
|
+
# Receives data with a 4-byte integer size prefix (network byte order).
|
3
|
+
# Underlying protocol must implement #send_data and invoke #receive_data.
|
4
|
+
# User's protocol must call #send_fast_message and listen to #receive_fast_message callback.
|
5
|
+
module FastMessageProtocol
|
6
|
+
def post_init
|
7
|
+
@fmp_size = 0 # if 0, we're waiting for a new message,
|
8
|
+
# else - accumulating data.
|
9
|
+
@fmp_size_chunk = "" # we store a part of size chunk here
|
10
|
+
@fmp_data = ""
|
11
|
+
super
|
12
|
+
end
|
13
|
+
|
14
|
+
LENGTH_FORMAT = "N".freeze
|
15
|
+
LENGTH_FORMAT_LENGTH = 4
|
16
|
+
|
17
|
+
def send_fast_message(data)
|
18
|
+
size = data.size
|
19
|
+
packed_size = [size].pack(LENGTH_FORMAT)
|
20
|
+
send_data packed_size
|
21
|
+
send_data data
|
22
|
+
end
|
23
|
+
|
24
|
+
def receive_data(next_chunk)
|
25
|
+
while true # helps fight deep recursion when receiving many messages in a single buffer.
|
26
|
+
data = next_chunk
|
27
|
+
# accumulate data
|
28
|
+
if @fmp_size > 0
|
29
|
+
left = @fmp_size - @fmp_data.size
|
30
|
+
now = data.size
|
31
|
+
log { "Need more #{left} bytes, got #{now} for now." }
|
32
|
+
|
33
|
+
if left > now
|
34
|
+
@fmp_data << data
|
35
|
+
break
|
36
|
+
elsif left == now
|
37
|
+
@fmp_data << data
|
38
|
+
data = @fmp_data
|
39
|
+
@fmp_data = ""
|
40
|
+
@fmp_size = 0
|
41
|
+
@fmp_size_chunk = ""
|
42
|
+
receive_fast_message(data)
|
43
|
+
break
|
44
|
+
else
|
45
|
+
# Received more, than expected.
|
46
|
+
# 1. Forward expected part
|
47
|
+
# 2. Put unexpected part into receive_data
|
48
|
+
@fmp_data << data[0, left]
|
49
|
+
next_chunk = data[left, now]
|
50
|
+
data = @fmp_data
|
51
|
+
@fmp_data = ""
|
52
|
+
@fmp_size = 0
|
53
|
+
@fmp_size_chunk = ""
|
54
|
+
log { "Returning #{data.size} bytes (#{data[0..32]})" }
|
55
|
+
receive_fast_message(data)
|
56
|
+
# (see while true: processing next chunk without recursive calls)
|
57
|
+
end
|
58
|
+
|
59
|
+
# get message size prefix
|
60
|
+
else
|
61
|
+
left = LENGTH_FORMAT_LENGTH - @fmp_size_chunk.size
|
62
|
+
now = data.size
|
63
|
+
log { "Need more #{left} bytes for size_chunk, got #{now} for now." }
|
64
|
+
|
65
|
+
if left > now
|
66
|
+
@fmp_size_chunk << data
|
67
|
+
break
|
68
|
+
elsif left == now
|
69
|
+
@fmp_size_chunk << data
|
70
|
+
@fmp_size = @fmp_size_chunk.unpack(LENGTH_FORMAT)[0]
|
71
|
+
log { "Ready to receive #{@fmp_size} bytes."}
|
72
|
+
break
|
73
|
+
else
|
74
|
+
# Received more, than expected.
|
75
|
+
# 1. Pick only expected part for length
|
76
|
+
# 2. Pass unexpected part into receive_data
|
77
|
+
@fmp_size_chunk << data[0, left]
|
78
|
+
next_chunk = data[left, now]
|
79
|
+
@fmp_size = @fmp_size_chunk.unpack(LENGTH_FORMAT)[0]
|
80
|
+
log { "Ready to receive #{@fmp_size} bytes."}
|
81
|
+
# (see while true) receive_data(next_chunk) # process next chunk
|
82
|
+
end # if
|
83
|
+
end # if
|
84
|
+
end # while true
|
85
|
+
end # def receive_data
|
86
|
+
|
87
|
+
def err
|
88
|
+
STDERR.write("FastMessageProtocol: #{yield}\n")
|
89
|
+
end
|
90
|
+
def log
|
91
|
+
puts("FastMessageProtocol: #{yield}")
|
92
|
+
end
|
93
|
+
# Switch logging off when not in debug mode.
|
94
|
+
unless $DEBUG || ENV['DEBUG']
|
95
|
+
def log
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end # module FastMessageProtocol
|
99
|
+
|
100
|
+
# Allows to use send_message/receive_message without
|
101
|
+
# a knowledge of the particular messaging protocol.
|
102
|
+
module FastMessageProtocolAdapter
|
103
|
+
def send_message(msg)
|
104
|
+
send_fast_message(msg)
|
105
|
+
end
|
106
|
+
def receive_fast_message(msg)
|
107
|
+
receive_message(msg)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
end # EMRPC
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module EMRPC
|
2
|
+
module MarshalProtocol
|
3
|
+
# Creates new protocol using specified dump/load interface.
|
4
|
+
# Note: interface must be a constant! See examples below.
|
5
|
+
# Examples:
|
6
|
+
# 1. include MarshalProtocol.new(Marshal)
|
7
|
+
# 2. include MarshalProtocol.new(YAML)
|
8
|
+
# 3. include MarshalProtocol.new(JSON)
|
9
|
+
def self.new(marshal_const)
|
10
|
+
const_name = marshal_const.name
|
11
|
+
mod = Module.new
|
12
|
+
mod.class_eval <<-EOF, __FILE__, __LINE__
|
13
|
+
def send_marshalled_message(msg)
|
14
|
+
send_message(#{const_name}.dump(msg))
|
15
|
+
end
|
16
|
+
def receive_message(msg)
|
17
|
+
receive_marshalled_message(#{const_name}.load(msg))
|
18
|
+
rescue => e
|
19
|
+
rescue_marshal_error(e)
|
20
|
+
end
|
21
|
+
EOF
|
22
|
+
mod
|
23
|
+
end
|
24
|
+
|
25
|
+
# Prevent direct inclusion by accident.
|
26
|
+
def self.included(base)
|
27
|
+
raise "#{self} cannot be included directly! Create a module using #{self}.new(marshal_const)."
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'thread'
|
2
|
+
module EMRPC
|
3
|
+
# Sends all the messages to a specified backend
|
4
|
+
# FIXME: deal with Object's methods gracefully.
|
5
|
+
class MethodProxy
|
6
|
+
EMPTY_ARGS = [ ].freeze
|
7
|
+
attr_reader :__emrpc_backend
|
8
|
+
def initialize(backend)
|
9
|
+
@__emrpc_backend = backend
|
10
|
+
end
|
11
|
+
|
12
|
+
def method_missing(meth, *args, &blk)
|
13
|
+
@__emrpc_backend.send_message(meth, args, blk)
|
14
|
+
end
|
15
|
+
|
16
|
+
def id
|
17
|
+
@__emrpc_backend.send_message(:id, EMPTY_ARGS, nil)
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_s
|
21
|
+
@__emrpc_backend.send_message(:to_s, EMPTY_ARGS, nil)
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_str
|
25
|
+
@__emrpc_backend.send_message(:to_str, EMPTY_ARGS, nil)
|
26
|
+
end
|
27
|
+
|
28
|
+
alias :__class__ :class
|
29
|
+
def class
|
30
|
+
@__emrpc_backend.send_message(:class, EMPTY_ARGS, nil)
|
31
|
+
end
|
32
|
+
|
33
|
+
def inspect
|
34
|
+
"#<#{self.__class__.name}:0x#{__id__.to_s(16)} remote:#{@__emrpc_backend.send_message(:inspect, EMPTY_ARGS, nil)}>"
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'thread'
|
2
|
+
module EMRPC
|
3
|
+
class PoolTimeout < StandardError; end
|
4
|
+
class MultithreadedClient
|
5
|
+
attr_reader :pool, :backends, :timeout
|
6
|
+
def initialize(options)
|
7
|
+
@backends = options[:backends] or raise "No backends supplied!"
|
8
|
+
@pool = options[:queue] || ::Queue.new
|
9
|
+
@timeout = options[:timeout] || 5
|
10
|
+
@timeout_thread = Thread.new { timer_action! }
|
11
|
+
@backends.each do |backend|
|
12
|
+
@pool.push(backend)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def send_message(meth, args, blk)
|
17
|
+
start = Time.now
|
18
|
+
# wait for the available connections here
|
19
|
+
while :timeout == (backend = @pool.pop)
|
20
|
+
seconds = Time.now - start
|
21
|
+
if seconds > @timeout
|
22
|
+
raise PoolTimeout, "Thread #{Thread.current} waited #{seconds} seconds for backend connection in a pool. Pool size is #{@backends.size}. Maybe too many threads are running concurrently. Increase the pool size or decrease the number of threads."
|
23
|
+
end
|
24
|
+
end
|
25
|
+
begin
|
26
|
+
backend.send_message(meth, args, blk)
|
27
|
+
ensure # Always push backend to a pool after using it!
|
28
|
+
@pool.push(backend)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Pushes :timeout message to a queue for all
|
33
|
+
# the threads in a backlog every @timeout seconds.
|
34
|
+
def timer_action!
|
35
|
+
sleep @timeout
|
36
|
+
@pool.num_waiting.times { @pool.push(:timeout) }
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module EMRPC
|
2
|
+
# Reconnects when needed.
|
3
|
+
class ReconnectingClient
|
4
|
+
attr_reader :host, :port, :protocol
|
5
|
+
def initialize(options)
|
6
|
+
@host = options[:host]
|
7
|
+
@port = options[:port]
|
8
|
+
@protocol = options[:protocol]
|
9
|
+
@timeout = options[:timeout]
|
10
|
+
|
11
|
+
@connection = nil # diconnected at the start
|
12
|
+
end
|
13
|
+
|
14
|
+
def connection
|
15
|
+
return @connection if @connection
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
def send_data(data)
|
20
|
+
connection.send_data
|
21
|
+
end
|
22
|
+
|
23
|
+
def close_connection(after_writing = false)
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
module ReconnectingCallbacks
|
28
|
+
attr_accessor :reconnecting_client
|
29
|
+
|
30
|
+
def receive_data(data)
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
def connection_completed
|
35
|
+
@reconnecting_client.connection_completed
|
36
|
+
end
|
37
|
+
|
38
|
+
def unbind
|
39
|
+
@reconnecting_client.unbind
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end # ReconnectingClient
|
43
|
+
|
44
|
+
# Raised when reconnection timeout is over.
|
45
|
+
class ReconnectionTimeout < StandardError; end
|
46
|
+
end
|
data/lib/emrpc/server.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
module EMRPC
|
2
|
+
class Server
|
3
|
+
attr_accessor :host, :port, :object, :protocol
|
4
|
+
def initialize(options = {})
|
5
|
+
@host = options[:host]
|
6
|
+
@port = options[:port]
|
7
|
+
@object = options[:object]
|
8
|
+
@protocol = options[:protocol] || ServerProtocol
|
9
|
+
end
|
10
|
+
|
11
|
+
def run
|
12
|
+
EventMachine.start_server(@host, @port, @protocol) do |conn|
|
13
|
+
conn.__served_object = @object
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
data/lib/emrpc.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'eventmachine'
|
3
|
+
|
4
|
+
$LOAD_PATH.unshift( File.expand_path(File.join(File.dirname(__FILE__))))
|
5
|
+
|
6
|
+
require 'emrpc/version'
|
7
|
+
require 'emrpc/fast_message_protocol'
|
8
|
+
require 'emrpc/marshal_protocol'
|
9
|
+
require 'emrpc/server'
|
10
|
+
require 'emrpc/method_proxy'
|
11
|
+
require 'emrpc/multithreaded_client'
|
12
|
+
require 'emrpc/singlethreaded_client'
|
13
|
+
require 'emrpc/client'
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
describe FastMessageProtocol do
|
4
|
+
before(:each) do
|
5
|
+
@peer_class = Class.new do
|
6
|
+
def initialize
|
7
|
+
post_init
|
8
|
+
end
|
9
|
+
def post_init
|
10
|
+
end
|
11
|
+
end
|
12
|
+
@oleganza = Class.new(@peer_class) do
|
13
|
+
include FastMessageProtocol
|
14
|
+
end
|
15
|
+
@yrashk = Class.new(@peer_class) do
|
16
|
+
include FastMessageProtocol
|
17
|
+
end
|
18
|
+
@oleganza.class_eval do
|
19
|
+
attr_accessor :messages
|
20
|
+
def post_init
|
21
|
+
@messages = []
|
22
|
+
super
|
23
|
+
end
|
24
|
+
def receive_fast_message(msg)
|
25
|
+
@messages << msg
|
26
|
+
end
|
27
|
+
end
|
28
|
+
@yrashk.class_eval do
|
29
|
+
attr_accessor :peer
|
30
|
+
def post_init
|
31
|
+
@buffer = ""
|
32
|
+
super
|
33
|
+
end
|
34
|
+
def send_data(data)
|
35
|
+
@buffer << data
|
36
|
+
flush_buffer if @buffer.size > (rand(300) + 1)
|
37
|
+
end
|
38
|
+
def flush_buffer
|
39
|
+
@peer.receive_data(@buffer.dup)
|
40
|
+
@buffer = ""
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should receive all messages" do
|
46
|
+
messages = Array.new(1000) {|i| (i*(rand(100)+1)).to_s*(1+rand(100)) }
|
47
|
+
oleg = @oleganza.new
|
48
|
+
yr = @yrashk.new
|
49
|
+
yr.peer = oleg
|
50
|
+
messages.each {|m| yr.send_fast_message(m) }
|
51
|
+
yr.flush_buffer
|
52
|
+
|
53
|
+
oleg.messages.should == messages
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should handle protocol errors" do
|
57
|
+
pending "Add message size limit!"
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
def create_marshal_protocol_instance(interface)
|
7
|
+
mod = MarshalProtocol.new(interface)
|
8
|
+
|
9
|
+
spec_module = Module.new do
|
10
|
+
def send_message(data)
|
11
|
+
data
|
12
|
+
end
|
13
|
+
def receive_marshalled_message(data)
|
14
|
+
data
|
15
|
+
end
|
16
|
+
def rescue_marshal_error(e)
|
17
|
+
[:error, e]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
Class.new do
|
22
|
+
include mod
|
23
|
+
include spec_module
|
24
|
+
end.new
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "Generic MarshalProtocol", :shared => true do
|
28
|
+
it "should pass data in and out" do
|
29
|
+
encoded = @instance.send_marshalled_message(@msg)
|
30
|
+
decoded = @instance.receive_message(encoded)
|
31
|
+
decoded.should == @msg
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should report error when format is wrong" do
|
35
|
+
encoded = @instance.send_marshalled_message(@msg)
|
36
|
+
decoded = @instance.receive_message("blah-blah"+encoded)
|
37
|
+
decoded.first.should == :error
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
[Marshal, JSON, YAML].each do |interface|
|
42
|
+
describe MarshalProtocol, "with #{interface}" do
|
43
|
+
before(:each) do
|
44
|
+
# FIXME: fixture containing 3.1415 may cause floating point issues.
|
45
|
+
@msg = ["Hello", {"a" => "b", "arr" => [true, false, nil]}, 1, 3.1415]
|
46
|
+
@instance = create_marshal_protocol_instance(interface)
|
47
|
+
end
|
48
|
+
it_should_behave_like "Generic MarshalProtocol"
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
describe MethodProxy do
|
4
|
+
before(:each) do
|
5
|
+
@backend = [ 1, 2, 3 ]
|
6
|
+
class <<@backend
|
7
|
+
def send_message(meth, args, blk)
|
8
|
+
send(meth, *args, &blk)
|
9
|
+
end
|
10
|
+
def id
|
11
|
+
object_id
|
12
|
+
end
|
13
|
+
end
|
14
|
+
@proxy = MethodProxy.new(@backend)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should proxy regular methods" do
|
18
|
+
@proxy.size.should == @backend.size
|
19
|
+
(@proxy*2).should == (@backend*2)
|
20
|
+
@proxy.join(':').should == @backend.join(':')
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should proxy Object's methods" do
|
24
|
+
@proxy.to_s.should == @backend.to_s
|
25
|
+
lambda { @proxy.to_str.should == @backend.to_str }.should raise_error(NoMethodError) # to_str is undefined
|
26
|
+
@proxy.class.should == @backend.class
|
27
|
+
@proxy.id.should == @backend.id
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should inspect" do
|
31
|
+
@proxy.inspect == %{#<#{MethodProxy}:0x#{@proxy.__id__.to_s(16)} remote:#{@backend.inspect}>}
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
describe MultithreadedClient, " with no timeouts" do
|
4
|
+
include ThreadHelpers
|
5
|
+
before(:each) do
|
6
|
+
@mod = Module.new do
|
7
|
+
def send_message(meth, args, blk)
|
8
|
+
send(meth, *args, &blk)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
@backends = [ [ 1 ], [ 2 ], [ 3 ] ]
|
12
|
+
@backends.each{|b| b.extend(@mod) }
|
13
|
+
|
14
|
+
@client = MultithreadedClient.new(:backends => @backends, :timeout => 1)
|
15
|
+
end
|
16
|
+
it "should work with all backends" do
|
17
|
+
ts = create_threads(10) do
|
18
|
+
loop { @client.send_message(:[], [ 0 ], nil) }
|
19
|
+
end
|
20
|
+
sleep 3
|
21
|
+
ts.each {|t| t.kill}
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe MultithreadedClient, " with PoolTimeout" do
|
26
|
+
include ThreadHelpers
|
27
|
+
before(:each) do
|
28
|
+
@long_backend = Object.new
|
29
|
+
class <<@long_backend
|
30
|
+
def send_message(meth, args, blk)
|
31
|
+
sleep 0.5
|
32
|
+
end
|
33
|
+
end
|
34
|
+
@long_client = MultithreadedClient.new(:backends => [ @long_backend ], :timeout => 1)
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should raise ThreadTimeout" do
|
38
|
+
ts = create_threads(50) do # some of them will die
|
39
|
+
@long_client.send_message(:some_meth, nil, nil)
|
40
|
+
end
|
41
|
+
create_threads(10, true) do
|
42
|
+
lambda {
|
43
|
+
@long_client.send_message(:some_meth, nil, nil)
|
44
|
+
}.should raise_error(PoolTimeout)
|
45
|
+
end
|
46
|
+
sleep 3
|
47
|
+
ts.each {|t| t.kill}
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
describe SinglethreadedClient do
|
4
|
+
include ThreadHelpers
|
5
|
+
before(:each) do
|
6
|
+
@backend= [1, 2, 3]
|
7
|
+
class <<@backend
|
8
|
+
def send_message(meth, args, blk)
|
9
|
+
send(meth, *args, &blk)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
@client = SinglethreadedClient.new(:backend => @backend, :timeout => 1)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should work without errors with regular methods" do
|
16
|
+
@client.send_message(:size, nil, nil).should == 3
|
17
|
+
@client.send_message(:join, [":"], nil).should == "1:2:3"
|
18
|
+
@client.send_message(:map, nil, proc{|e| e.to_s}).should == %w[1 2 3]
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
|
23
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
$LOAD_PATH.unshift( File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) )
|
2
|
+
|
3
|
+
require 'emrpc'
|
4
|
+
include EMRPC
|
5
|
+
|
6
|
+
EM_HOST = ENV['EM_HOST'] || "127.0.0.1"
|
7
|
+
EM_PORT = (ENV['EM_PORT'] || 4567).to_i
|
8
|
+
|
9
|
+
module Fixtures
|
10
|
+
class Person
|
11
|
+
attr_accessor :name
|
12
|
+
def initialize(options = {})
|
13
|
+
@name = options[:name]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class Paris
|
18
|
+
attr_accessor :people, :options
|
19
|
+
|
20
|
+
def initialize(ppl, options = {})
|
21
|
+
@options = options
|
22
|
+
@people = Array.new(ppl){ Person.new }
|
23
|
+
@name = "Paris"
|
24
|
+
end
|
25
|
+
|
26
|
+
def translate(english_word)
|
27
|
+
"le #{english_word}" # :-)
|
28
|
+
end
|
29
|
+
|
30
|
+
def visit(person)
|
31
|
+
people << person
|
32
|
+
people.size
|
33
|
+
end
|
34
|
+
|
35
|
+
def run_exception
|
36
|
+
raise SomeException, "paris message"
|
37
|
+
end
|
38
|
+
class SomeException < Exception; end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
module ThreadHelpers
|
43
|
+
def create_threads(n, abort_on_exception = false, &blk)
|
44
|
+
Array.new(n) do
|
45
|
+
t = Thread.new(&blk)
|
46
|
+
t.abort_on_exception = abort_on_exception
|
47
|
+
t
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Runs eventmachine reactor in a child thread,
|
53
|
+
# waits 0.5 sec. in a current thread.
|
54
|
+
# Returns child thread.
|
55
|
+
#
|
56
|
+
module EventMachine
|
57
|
+
def self.run_in_thread(delay = 0.5, &blk)
|
58
|
+
t = Thread.new do
|
59
|
+
EventMachine.run(&blk)
|
60
|
+
end
|
61
|
+
sleep delay
|
62
|
+
t
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
describe "Sync client" do
|
4
|
+
|
5
|
+
# Run server
|
6
|
+
before(:all) do
|
7
|
+
@em_thread = EM.run_in_thread do
|
8
|
+
paris = Fixtures::Paris.new(4, :cafes => ["quai-quai", "2 moulins"])
|
9
|
+
EMRPC::Server.new(:host => EM_HOST, :port => EM_PORT, :object => paris).run
|
10
|
+
end
|
11
|
+
end
|
12
|
+
after(:all) do
|
13
|
+
begin
|
14
|
+
EM.stop_event_loop
|
15
|
+
@em_thread.kill
|
16
|
+
rescue => e
|
17
|
+
puts "Exception after specs:"
|
18
|
+
puts e.inspect
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Run client
|
23
|
+
before(:each) do
|
24
|
+
@paris = EMRPC::Client.new(:host => EM_HOST, :port => EM_PORT)
|
25
|
+
# wrong port
|
26
|
+
@berlin = EMRPC::Client.new(:host => EM_HOST, :port => EM_PORT + 1)
|
27
|
+
# wrong host
|
28
|
+
@prague = EMRPC::Client.new(:host => "192.254.254.254", :port => EM_PORT)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should get serialized data" do
|
32
|
+
@paris.options.should == { :cafes => ["quai-quai", "2 moulins"] }
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should issue connection error" do
|
36
|
+
lambda { @berlin.options }.should raise_error(EMRPC::ConnectionError)
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
metadata
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: emrpc
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: "0.1"
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Oleg Andreev
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-08-18 00:00:00 +04:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: eventmachine
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: rake
|
27
|
+
type: :runtime
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: "0"
|
34
|
+
version:
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: rspec
|
37
|
+
type: :runtime
|
38
|
+
version_requirement:
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: "0"
|
44
|
+
version:
|
45
|
+
description: Efficient RPC library with evented and blocking APIs. In all ways better than DRb.
|
46
|
+
email: oleganza@gmail.com
|
47
|
+
executables: []
|
48
|
+
|
49
|
+
extensions: []
|
50
|
+
|
51
|
+
extra_rdoc_files:
|
52
|
+
- README
|
53
|
+
- TODO
|
54
|
+
files:
|
55
|
+
- README
|
56
|
+
- Rakefile
|
57
|
+
- TODO
|
58
|
+
- spec/fast_message_protocol_spec.rb
|
59
|
+
- spec/marshal_protocol_spec.rb
|
60
|
+
- spec/method_proxy_spec.rb
|
61
|
+
- spec/multithreaded_client_spec.rb
|
62
|
+
- spec/singlethreaded_client_spec.rb
|
63
|
+
- spec/spec_helper.rb
|
64
|
+
- spec/sync_client_spec.rb
|
65
|
+
- lib/emrpc
|
66
|
+
- lib/emrpc/client.rb
|
67
|
+
- lib/emrpc/em_connection.rb
|
68
|
+
- lib/emrpc/fast_message_protocol.rb
|
69
|
+
- lib/emrpc/marshal_protocol.rb
|
70
|
+
- lib/emrpc/method_proxy.rb
|
71
|
+
- lib/emrpc/multithreaded_client.rb
|
72
|
+
- lib/emrpc/reconnecting_client.rb
|
73
|
+
- lib/emrpc/server.rb
|
74
|
+
- lib/emrpc/singlethreaded_client.rb
|
75
|
+
- lib/emrpc/version.rb
|
76
|
+
- lib/emrpc.rb
|
77
|
+
has_rdoc: true
|
78
|
+
homepage: http://strokedb.com
|
79
|
+
post_install_message:
|
80
|
+
rdoc_options: []
|
81
|
+
|
82
|
+
require_paths:
|
83
|
+
- lib
|
84
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: 1.8.4
|
89
|
+
version:
|
90
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - ">="
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: "0"
|
95
|
+
version:
|
96
|
+
requirements:
|
97
|
+
- You need to install the json (or json_pure), yaml, rack gems to use related features.
|
98
|
+
rubyforge_project:
|
99
|
+
rubygems_version: 1.2.0
|
100
|
+
signing_key:
|
101
|
+
specification_version: 2
|
102
|
+
summary: Efficient RPC library with evented and blocking APIs. In all ways better than DRb.
|
103
|
+
test_files: []
|
104
|
+
|