rubarb 0.2.0
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/.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
|