rubarb 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +7 -0
- data/README +77 -0
- data/Rakefile +51 -0
- data/VERSION +1 -0
- data/examples/client.rb +32 -0
- data/examples/server.rb +34 -0
- data/lib/dkbrpc.rb +4 -0
- data/lib/rubarb.rb +2 -0
- data/lib/rubarb/connection.rb +170 -0
- data/lib/rubarb/connection_error.rb +11 -0
- data/lib/rubarb/connection_id.rb +11 -0
- data/lib/rubarb/default.rb +15 -0
- data/lib/rubarb/fast_message_protocol.rb +109 -0
- data/lib/rubarb/id.rb +13 -0
- data/lib/rubarb/incoming_connection.rb +25 -0
- data/lib/rubarb/insecure_method_call_error.rb +11 -0
- data/lib/rubarb/outgoing_connection.rb +27 -0
- data/lib/rubarb/remote_call.rb +12 -0
- data/lib/rubarb/responder.rb +18 -0
- data/lib/rubarb/server.rb +185 -0
- data/spec/rubarb/connection_failure_spec.rb +71 -0
- data/spec/rubarb/connection_spec.rb +187 -0
- data/spec/rubarb/id_spec.rb +26 -0
- data/spec/rubarb/incoming_connection_spec.rb +79 -0
- data/spec/rubarb/integration_spec.rb +174 -0
- data/spec/rubarb/outgoing_connection_spec.rb +103 -0
- data/spec/rubarb/remote_call_spec.rb +48 -0
- data/spec/rubarb/responder_spec.rb +35 -0
- data/spec/rubarb/server_failure_spec.rb +261 -0
- data/spec/rubarb/server_spec.rb +201 -0
- data/spec/spec_helper.rb +32 -0
- metadata +146 -0
data/.gitignore
ADDED
data/README
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
rubaRb
|
2
|
+
A Bidirectional Event Machine Based Remote Procedure Call Library for Ruby
|
3
|
+
|
4
|
+
This library uses two socket connections between a client and a server.
|
5
|
+
One is used for request / replies from the client to the server.
|
6
|
+
The other is used for remote calls made from the server to the client.
|
7
|
+
|
8
|
+
Each end publishes a single object on which methods can be called by the remote end.
|
9
|
+
All calls to the remote objects are asyncronous. Do not make any blocking calls in
|
10
|
+
the published object. Responses are return by calling the "reply method on the responder object.
|
11
|
+
|
12
|
+
Server and Connection object may be created and started outside of EM::run,
|
13
|
+
but the Eventmachine reactor must be started somewhere in your application
|
14
|
+
|
15
|
+
################################# Server Example #####################################################################
|
16
|
+
|
17
|
+
class ServerApi
|
18
|
+
def time(responder)
|
19
|
+
puts "Server received time request"
|
20
|
+
responder.reply(Time.now)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
EM.run do
|
25
|
+
server = Rubarb::Server.new("127.0.0.1", 9441, ServerApi.new)
|
26
|
+
|
27
|
+
connections = {}
|
28
|
+
|
29
|
+
server.start do |client|
|
30
|
+
puts "Connection Made: #{client}"
|
31
|
+
client.name do |name|
|
32
|
+
connections[name] = client
|
33
|
+
client.errback do
|
34
|
+
puts "Connection Lost: #{name}"
|
35
|
+
connections.delete(name)
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
EventMachine.add_periodic_timer(1) { puts "Connections: #{connections.keys.inspect}" }
|
43
|
+
|
44
|
+
end
|
45
|
+
######################################################################################################################
|
46
|
+
|
47
|
+
|
48
|
+
################################# Client Example #####################################################################
|
49
|
+
class ClientApi
|
50
|
+
def initialize(name)
|
51
|
+
@name = name
|
52
|
+
end
|
53
|
+
def name(responder)
|
54
|
+
responder.reply(@name)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
EM::run do
|
59
|
+
connection = Rubarb::Connection.new("127.0.0.1", 9441, ClientApi.new(ARGV[0]))
|
60
|
+
connection.errback do |error|
|
61
|
+
puts ("Connection Error: #{error}")
|
62
|
+
end
|
63
|
+
|
64
|
+
connection.start do
|
65
|
+
connection.time do |response|
|
66
|
+
puts "Server Said it is: #{response.strftime("%D")}"
|
67
|
+
end
|
68
|
+
|
69
|
+
EventMachine.add_timer(20) do
|
70
|
+
puts "stopping"
|
71
|
+
connection.stop
|
72
|
+
EM::stop
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
######################################################################################################################
|
data/Rakefile
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "rubarb"
|
8
|
+
gem.summary = %Q{A Bidirectional Event Machine Based Remote Procedure Call Library for Ruby}
|
9
|
+
gem.description = %Q{This library uses two socket connections between a client and a server. One is used for request / replies from the
|
10
|
+
client to the server. The other is used for remote calls made from the server to the client.
|
11
|
+
|
12
|
+
Each end publishes a single object on which
|
13
|
+
methods can be called by the remote end. All calls to the remote objects are asyncronous. Do not make any blocking
|
14
|
+
calls in the published object. Responses are return by calling the "reply method on the responder object.}
|
15
|
+
|
16
|
+
gem.email = "doug@8thlight.com"
|
17
|
+
gem.homepage = "http://github.com/dougbradbury/rubarb"
|
18
|
+
gem.authors = ["doug bradbury"]
|
19
|
+
gem.add_development_dependency "rspec", ">= 1.3.0"
|
20
|
+
gem.add_dependency "eventmachine", ">= 0.12.11"
|
21
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
22
|
+
end
|
23
|
+
rescue LoadError
|
24
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
25
|
+
end
|
26
|
+
|
27
|
+
require 'spec/rake/spectask'
|
28
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
29
|
+
spec.libs << 'lib' << 'spec'
|
30
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
31
|
+
end
|
32
|
+
|
33
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
34
|
+
spec.libs << 'lib' << 'spec'
|
35
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
36
|
+
spec.rcov = true
|
37
|
+
end
|
38
|
+
|
39
|
+
task :spec => :check_dependencies
|
40
|
+
|
41
|
+
task :default => :spec
|
42
|
+
|
43
|
+
require 'rake/rdoctask'
|
44
|
+
Rake::RDocTask.new do |rdoc|
|
45
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
46
|
+
|
47
|
+
rdoc.rdoc_dir = 'rdoc'
|
48
|
+
rdoc.title = "rubarb-gem #{version}"
|
49
|
+
rdoc.rdoc_files.include('README*')
|
50
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
51
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.2.0
|
data/examples/client.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
$: << File.expand_path(File.dirname(__FILE__) + '/../lib')
|
2
|
+
require 'rubygems'
|
3
|
+
require 'rubarb'
|
4
|
+
|
5
|
+
class ClientApi
|
6
|
+
def initialize(name)
|
7
|
+
@name = name
|
8
|
+
end
|
9
|
+
def name(responder)
|
10
|
+
responder.reply(@name)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
EM::run do
|
15
|
+
connection = Rubarb::Connection.new("127.0.0.1", 9441, ClientApi.new(ARGV[0]))
|
16
|
+
connection.errback do |error|
|
17
|
+
puts ("Connection Error: #{error}")
|
18
|
+
end
|
19
|
+
|
20
|
+
connection.start do
|
21
|
+
connection.time do |response|
|
22
|
+
puts "Server Said it is: #{response.strftime("%D")}"
|
23
|
+
end
|
24
|
+
|
25
|
+
EventMachine.add_timer(20) do
|
26
|
+
puts "stopping"
|
27
|
+
connection.stop
|
28
|
+
EM::stop
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
data/examples/server.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
$: << File.expand_path(File.dirname(__FILE__) + '/../lib')
|
2
|
+
#$DEBUG = true
|
3
|
+
require 'rubygems'
|
4
|
+
require 'rubarb'
|
5
|
+
|
6
|
+
|
7
|
+
class ServerApi
|
8
|
+
def time(responder)
|
9
|
+
puts "Server received time request"
|
10
|
+
responder.reply(Time.now)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
EM.run do
|
15
|
+
server = Rubarb::Server.new("127.0.0.1", 9441, ServerApi.new)
|
16
|
+
|
17
|
+
connections = {}
|
18
|
+
|
19
|
+
server.start do |client|
|
20
|
+
puts "Connection Made: #{client}"
|
21
|
+
client.name do |name|
|
22
|
+
connections[name] = client
|
23
|
+
client.errback do
|
24
|
+
puts "Connection Lost: #{name}"
|
25
|
+
connections.delete(name)
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
EventMachine.add_periodic_timer(1) { puts "Connections: #{connections.keys.inspect}" }
|
33
|
+
|
34
|
+
end
|
data/lib/dkbrpc.rb
ADDED
data/lib/rubarb.rb
ADDED
@@ -0,0 +1,170 @@
|
|
1
|
+
require "rubarb/connection_id"
|
2
|
+
require "rubarb/fast_message_protocol"
|
3
|
+
require "rubarb/remote_call"
|
4
|
+
require 'rubarb/outgoing_connection'
|
5
|
+
require 'rubarb/incoming_connection'
|
6
|
+
require 'rubarb/connection_error'
|
7
|
+
require 'rubarb/default'
|
8
|
+
|
9
|
+
module Rubarb
|
10
|
+
module IncomingHandler
|
11
|
+
include ConnectionId
|
12
|
+
|
13
|
+
attr_accessor :id
|
14
|
+
attr_accessor :on_connection
|
15
|
+
attr_accessor :api
|
16
|
+
attr_accessor :errbacks
|
17
|
+
attr_accessor :insecure_methods
|
18
|
+
|
19
|
+
def post_init
|
20
|
+
@buffer = ""
|
21
|
+
end
|
22
|
+
|
23
|
+
def connection_completed
|
24
|
+
Rubarb::FastMessageProtocol.install(self)
|
25
|
+
send_data("5")
|
26
|
+
send_data(@id)
|
27
|
+
end
|
28
|
+
|
29
|
+
def receive_message message
|
30
|
+
if (message == @id)
|
31
|
+
self.extend(Rubarb::IncomingConnection)
|
32
|
+
@on_connection.call if @on_connection
|
33
|
+
else
|
34
|
+
call_errbacks(ConnectionError.new("Handshake Failure"))
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def unbind
|
39
|
+
call_errbacks(ConnectionError.new)
|
40
|
+
end
|
41
|
+
|
42
|
+
def call_errbacks(message)
|
43
|
+
@errbacks.each do |e|
|
44
|
+
e.call(message)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
module OutgoingHandler
|
51
|
+
include ConnectionId
|
52
|
+
include OutgoingConnection
|
53
|
+
attr_accessor :host
|
54
|
+
attr_accessor :port
|
55
|
+
attr_accessor :on_connection
|
56
|
+
attr_accessor :api
|
57
|
+
attr_accessor :errbacks
|
58
|
+
attr_accessor :callback
|
59
|
+
attr_accessor :msg_id_generator
|
60
|
+
attr_accessor :insecure_methods
|
61
|
+
|
62
|
+
def post_init
|
63
|
+
@buffer = ""
|
64
|
+
end
|
65
|
+
|
66
|
+
def connection_completed
|
67
|
+
send_data("4")
|
68
|
+
end
|
69
|
+
|
70
|
+
def receive_data data
|
71
|
+
@buffer << data
|
72
|
+
if @id.nil?
|
73
|
+
handshake(@buffer)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def unbind
|
78
|
+
if @incoming_connection
|
79
|
+
EM.next_tick { @incoming_connection.close_connection }
|
80
|
+
else
|
81
|
+
call_errbacks(ConnectionError.new)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def call_errbacks(message)
|
86
|
+
@errbacks.each do |e|
|
87
|
+
e.call(message)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def handshake(buffer)
|
94
|
+
if complete_id?(buffer)
|
95
|
+
Rubarb::FastMessageProtocol.install(self)
|
96
|
+
EventMachine::connect(@host, @port, IncomingHandler) do |incoming_connection|
|
97
|
+
@id = extract_id(buffer)
|
98
|
+
incoming_connection.id = @id
|
99
|
+
incoming_connection.on_connection = @on_connection
|
100
|
+
incoming_connection.api = @api
|
101
|
+
incoming_connection.errbacks = @errbacks
|
102
|
+
incoming_connection.insecure_methods = @insecure_methods
|
103
|
+
@incoming_connection = incoming_connection
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
class Connection
|
110
|
+
attr_reader :remote_connection
|
111
|
+
attr_reader :msg_id_generator
|
112
|
+
|
113
|
+
def initialize(host, port, api, insecure_methods=Default::INSECURE_METHODS)
|
114
|
+
@host = host
|
115
|
+
@port = port
|
116
|
+
@api = api
|
117
|
+
@msg_id_generator = Id.new
|
118
|
+
@errbacks = []
|
119
|
+
@insecure_methods = insecure_methods
|
120
|
+
end
|
121
|
+
|
122
|
+
def errback & block
|
123
|
+
@errbacks << block if block
|
124
|
+
end
|
125
|
+
|
126
|
+
def start & block
|
127
|
+
EventMachine::schedule do
|
128
|
+
begin
|
129
|
+
EventMachine::connect(@host, @port, OutgoingHandler) do |connection|
|
130
|
+
connection.host = @host
|
131
|
+
connection.port = @port
|
132
|
+
connection.on_connection = block
|
133
|
+
connection.api = @api
|
134
|
+
connection.errbacks = @errbacks
|
135
|
+
connection.msg_id_generator = @msg_id_generator
|
136
|
+
connection.insecure_methods = @insecure_methods
|
137
|
+
@remote_connection = connection
|
138
|
+
end
|
139
|
+
rescue Exception => e
|
140
|
+
@errbacks.each do |errback|
|
141
|
+
errback.call(e)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def method_missing(method, * args, & block)
|
148
|
+
EventMachine::schedule do
|
149
|
+
begin
|
150
|
+
@remote_connection.remote_call(method, args, & block)
|
151
|
+
rescue Exception => e
|
152
|
+
@remote_connection.call_errbacks(e)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def stop(& callback)
|
158
|
+
EventMachine::schedule do
|
159
|
+
if @remote_connection
|
160
|
+
EventMachine::next_tick do
|
161
|
+
@remote_connection.close_connection
|
162
|
+
callback.call(true) if callback
|
163
|
+
end
|
164
|
+
else
|
165
|
+
callback.call(false) if callback
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Rubarb
|
2
|
+
module Default
|
3
|
+
INSECURE_METHODS = [:==, :===, :=~, :__drbref, :__drburi, :__id__, :__send__, :_dump,
|
4
|
+
:class, :clone, :display, :dup, :enum_for, :eql?, :equal?, :extend, :freeze,
|
5
|
+
:frozen?, :hash, :id, :inspect, :instance_eval, :instance_exec, :instance_of?, :instance_variable_defined?,
|
6
|
+
:instance_variable_get, :instance_variable_set, :is_a?, :kind_of?, :method, :method_missing,
|
7
|
+
:methods, :nil?, :object_id, :pretty_print, :pretty_print_cycle, :private_methods,
|
8
|
+
:protected_methods, :public_methods, :respond_to?, :send, :singleton_methods,
|
9
|
+
:taint, :tainted?, :tap, :to_enum, :type, :untaint,
|
10
|
+
:add_options, :args_and_options, :as_null_object, :com, :config, :config=, :context, :dclone, :debugger,
|
11
|
+
:handle_different_imports, :hash, :include_class, :java, :java_kind_of?, :javax, :log, :null_object?, :org,
|
12
|
+
:received_message?, :taguri, :taguri=, :taint, :tainted?, :tap, :timeout, :to_a, :to_channel, :to_inputstream,
|
13
|
+
:to_outputstream, :to_s, :to_yaml, :to_yaml_properties, :to_yaml_style]
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module Rubarb
|
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_message and listen to #receive_message callback.
|
5
|
+
module FastMessageProtocol
|
6
|
+
|
7
|
+
def self.install(object)
|
8
|
+
object.extend Rubarb::FastMessageProtocol
|
9
|
+
object.post_init
|
10
|
+
end
|
11
|
+
|
12
|
+
def post_init
|
13
|
+
@fmp_size = 0 # if 0, we're waiting for a new message,
|
14
|
+
# else - accumulating data.
|
15
|
+
@fmp_size_chunk = "" # we store a part of size chunk here
|
16
|
+
@fmp_data = ""
|
17
|
+
end
|
18
|
+
|
19
|
+
LENGTH_FORMAT = "N".freeze
|
20
|
+
LENGTH_FORMAT_LENGTH = 4
|
21
|
+
|
22
|
+
def send_message(data)
|
23
|
+
size = data.size
|
24
|
+
packed_size = [size].pack(LENGTH_FORMAT)
|
25
|
+
send_data packed_size
|
26
|
+
send_data data
|
27
|
+
end
|
28
|
+
|
29
|
+
def receive_data(next_chunk)
|
30
|
+
log {"fmp rx: #{next_chunk}"}
|
31
|
+
while true # helps fight deep recursion when receiving many messages in a single buffer.
|
32
|
+
data = next_chunk
|
33
|
+
# accumulate data
|
34
|
+
if @fmp_size > 0
|
35
|
+
left = @fmp_size - @fmp_data.size
|
36
|
+
now = data.size
|
37
|
+
log { "Need more #{left} bytes, got #{now} for now." }
|
38
|
+
|
39
|
+
if left > now
|
40
|
+
@fmp_data << data
|
41
|
+
break
|
42
|
+
elsif left == now
|
43
|
+
@fmp_data << data
|
44
|
+
data = @fmp_data
|
45
|
+
@fmp_data = ""
|
46
|
+
@fmp_size = 0
|
47
|
+
@fmp_size_chunk = ""
|
48
|
+
receive_message(data)
|
49
|
+
break
|
50
|
+
else
|
51
|
+
# Received more, than expected.
|
52
|
+
# 1. Forward expected part
|
53
|
+
# 2. Put unexpected part into receive_data
|
54
|
+
@fmp_data << data[0, left]
|
55
|
+
next_chunk = data[left, now]
|
56
|
+
data = @fmp_data
|
57
|
+
@fmp_data = ""
|
58
|
+
@fmp_size = 0
|
59
|
+
@fmp_size_chunk = ""
|
60
|
+
log { "Returning #{data.size} bytes (#{data[0..32]})" }
|
61
|
+
receive_message(data)
|
62
|
+
# (see while true: processing next chunk without recursive calls)
|
63
|
+
end
|
64
|
+
|
65
|
+
# get message size prefix
|
66
|
+
else
|
67
|
+
left = LENGTH_FORMAT_LENGTH - @fmp_size_chunk.size
|
68
|
+
now = data.size
|
69
|
+
log { "Need more #{left} bytes for size_chunk, got #{now} for now." }
|
70
|
+
|
71
|
+
if left > now
|
72
|
+
@fmp_size_chunk << data
|
73
|
+
break
|
74
|
+
elsif left == now
|
75
|
+
@fmp_size_chunk << data
|
76
|
+
@fmp_size = @fmp_size_chunk.unpack(LENGTH_FORMAT)[0]
|
77
|
+
log { "Ready to receive #{@fmp_size} bytes." }
|
78
|
+
break
|
79
|
+
else
|
80
|
+
# Received more, than expected.
|
81
|
+
# 1. Pick only expected part for length
|
82
|
+
# 2. Pass unexpected part into receive_data
|
83
|
+
@fmp_size_chunk << data[0, left]
|
84
|
+
next_chunk = data[left, now]
|
85
|
+
@fmp_size = @fmp_size_chunk.unpack(LENGTH_FORMAT)[0]
|
86
|
+
log { "Ready to receive #{@fmp_size} bytes." }
|
87
|
+
# (see while true) receive_data(next_chunk) # process next chunk
|
88
|
+
end # if
|
89
|
+
end # if
|
90
|
+
end # while true
|
91
|
+
end
|
92
|
+
|
93
|
+
# def receive_data
|
94
|
+
|
95
|
+
def err
|
96
|
+
STDERR.write("FastMessageProtocol: #{yield}\n")
|
97
|
+
end
|
98
|
+
|
99
|
+
def log
|
100
|
+
puts("FastMessageProtocol: #{yield}")
|
101
|
+
end
|
102
|
+
|
103
|
+
# Switch logging off when not in debug mode.
|
104
|
+
unless ($DEBUG || ENV['DEBUG']) && !($NO_FMP_DEBUG || ENV['NO_FMP_DEBUG'])
|
105
|
+
def log
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end # module FastMessageProtocol
|
109
|
+
end # EMRPC
|