emrpc 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/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
|
+
|