emrpc 0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,8 @@
1
+ reconnecting client
2
+ rack http server & http client
3
+ object serialization:
4
+ self (special case, return reference to the running )
5
+ other
6
+ unix sockets interface
7
+ resource discovery
8
+ ring/chord protocol
@@ -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
@@ -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
@@ -0,0 +1,16 @@
1
+ require 'thread'
2
+ module EMRPC
3
+ class ConnectionTimeout < StandardError; end
4
+ class SinglethreadedClient
5
+ def initialize(options)
6
+
7
+ end
8
+
9
+ def send_message(meth, args, blk)
10
+ start = Time.now
11
+ # TODO...
12
+
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,3 @@
1
+ module EMRPC
2
+ VERSION = "0.1"
3
+ 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
@@ -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
+