foreign_actor 0.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/.gitignore +7 -0
- data/Gemfile +17 -0
- data/Guardfile +8 -0
- data/LICENSE +22 -0
- data/README.md +44 -0
- data/Rakefile +27 -0
- data/bin/device +75 -0
- data/bin/puppetmaster.rb +100 -0
- data/docs/design.graphml +221 -0
- data/docs/design.png +0 -0
- data/example/Procfile +2 -0
- data/example/client.rb +27 -0
- data/example/common.rb +10 -0
- data/example/device.yml +7 -0
- data/example/worker.rb +42 -0
- data/example2/Procfile +2 -0
- data/example2/README.md +9 -0
- data/example2/device.yml +4 -0
- data/example2/node.rb +59 -0
- data/foreign_actor.gemspec +20 -0
- data/lib/foreign_actor/client.rb +90 -0
- data/lib/foreign_actor/reactor.rb +275 -0
- data/lib/foreign_actor/reactor_mailbox.rb +29 -0
- data/lib/foreign_actor/serializer.rb +16 -0
- data/lib/foreign_actor/version.rb +3 -0
- data/lib/foreign_actor.rb +15 -0
- data/specs/spec_helper.rb +27 -0
- data/specs/unit/reactor_spec.rb +125 -0
- data/specs/unit/serializer_spec.rb +17 -0
- metadata +129 -0
data/docs/design.png
ADDED
Binary file
|
data/example/Procfile
ADDED
data/example/client.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require_relative 'common'
|
2
|
+
|
3
|
+
class ClientGroup < Celluloid::SupervisionGroup
|
4
|
+
supervise ForeignActor::Reactor, :as => :xs_reactor
|
5
|
+
|
6
|
+
end
|
7
|
+
|
8
|
+
ClientGroup.run!
|
9
|
+
|
10
|
+
# cl = ForeignActor::Client.create(CLIENT_ENDPOINT)
|
11
|
+
cl = ForeignActor::Client.new(CLIENT_ENDPOINT)
|
12
|
+
|
13
|
+
cl.async.do_it(0)
|
14
|
+
|
15
|
+
loop do
|
16
|
+
f = []
|
17
|
+
|
18
|
+
p cl.do_it(2)
|
19
|
+
|
20
|
+
started_at = Time.now
|
21
|
+
4.times {|n| f << cl.future.do_it(n) }
|
22
|
+
|
23
|
+
p f.map(&:value)
|
24
|
+
|
25
|
+
elapsed = (Time.now - started_at)
|
26
|
+
puts "time: #{elapsed} seconds"
|
27
|
+
end
|
data/example/common.rb
ADDED
data/example/device.yml
ADDED
data/example/worker.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require_relative 'common'
|
2
|
+
|
3
|
+
class Worker
|
4
|
+
include Celluloid
|
5
|
+
|
6
|
+
def initialize(endpoint)
|
7
|
+
Actor[:xs_reactor].serve_actor!(endpoint, Actor.current)
|
8
|
+
end
|
9
|
+
|
10
|
+
def do_it(n)
|
11
|
+
# if n == 2
|
12
|
+
# raise "what ?"
|
13
|
+
# end
|
14
|
+
|
15
|
+
Kernel.sleep 1
|
16
|
+
|
17
|
+
# if n == 0
|
18
|
+
# puts "did it !"
|
19
|
+
# end
|
20
|
+
|
21
|
+
"toto #{$$}"
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
class RootGroup < Celluloid::SupervisionGroup
|
27
|
+
supervise ForeignActor::Reactor, :as => :xs_reactor
|
28
|
+
# supervise ForeignActor::WorkersSupervisor
|
29
|
+
supervise Worker, :as => :worker1, args: [WORKERS_ENDPOINT]
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
RootGroup.run!
|
34
|
+
|
35
|
+
# Celluloid::Actor[:workers].register_server(:worker1, Worker, WORKERS_ENDPOINT)
|
36
|
+
|
37
|
+
# Celluloid::Actor[:xs_reactor].serve_actor(WORKERS_ENDPOINT, Worker.new)
|
38
|
+
|
39
|
+
puts "Worker started."
|
40
|
+
|
41
|
+
trap("INT") { Celluloid.shutdown; exit }
|
42
|
+
sleep
|
data/example2/Procfile
ADDED
data/example2/README.md
ADDED
data/example2/device.yml
ADDED
data/example2/node.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
|
4
|
+
require 'foreign_actor'
|
5
|
+
|
6
|
+
$stdout.sync = true
|
7
|
+
|
8
|
+
CLIENT_ENDPOINT = 'tcp://127.0.0.1:7000'
|
9
|
+
WORKERS_ENDPOINT = 'tcp://127.0.0.1:7001'
|
10
|
+
|
11
|
+
node_id = ARGV[0] || "node#{rand(100)}"
|
12
|
+
|
13
|
+
class Worker
|
14
|
+
include Celluloid
|
15
|
+
|
16
|
+
def initialize(endpoint)
|
17
|
+
Actor[:xs_reactor].serve_actor!(endpoint, Actor.current)
|
18
|
+
end
|
19
|
+
|
20
|
+
def handle_it(token)
|
21
|
+
puts "Got #{token}"
|
22
|
+
Kernel.sleep(rand(2000).to_f / 1000)
|
23
|
+
"#{$$} : #{token}"
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
|
30
|
+
class RootGroup < Celluloid::SupervisionGroup
|
31
|
+
supervise ForeignActor::Reactor, :as => :xs_reactor
|
32
|
+
supervise Worker, :as => :worker1, args: [WORKERS_ENDPOINT]
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
RootGroup.run!
|
37
|
+
|
38
|
+
puts "Node #{node_id} started."
|
39
|
+
trap("INT") { Celluloid.shutdown; exit }
|
40
|
+
|
41
|
+
cl = ForeignActor::Client.new(CLIENT_ENDPOINT, 4)
|
42
|
+
|
43
|
+
task_id = 0
|
44
|
+
|
45
|
+
loop do
|
46
|
+
f = []
|
47
|
+
|
48
|
+
started_at = Time.now
|
49
|
+
4.times do |n|
|
50
|
+
task_id += 1
|
51
|
+
f << cl.future(:handle_it, "#{node_id}:#{task_id}")
|
52
|
+
end
|
53
|
+
|
54
|
+
p f.map(&:value)
|
55
|
+
|
56
|
+
elapsed = (Time.now - started_at)
|
57
|
+
puts "time: #{elapsed} seconds"
|
58
|
+
end
|
59
|
+
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/foreign_actor/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Julien Ammous"]
|
6
|
+
gem.email = ["schmurfy@gmail.com"]
|
7
|
+
gem.description = %q{Distributed Actors above Celluloid}
|
8
|
+
gem.summary = %q{Distributed Actors}
|
9
|
+
gem.homepage = ""
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = ['device']
|
13
|
+
gem.name = "foreign_actor"
|
14
|
+
gem.require_paths = ["lib"]
|
15
|
+
gem.version = ForeignWorker::VERSION
|
16
|
+
|
17
|
+
gem.add_dependency 'ffi-rxs', '~> 1.2.1'
|
18
|
+
gem.add_dependency 'celluloid', '= 0.12.3'
|
19
|
+
gem.add_dependency 'msgpack', '= 0.4.7'
|
20
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'celluloid'
|
2
|
+
|
3
|
+
module ForeignActor
|
4
|
+
|
5
|
+
State = Struct.new(:state) do
|
6
|
+
attr_accessor :state
|
7
|
+
end
|
8
|
+
|
9
|
+
class ClientProxy < Celluloid::ActorProxy
|
10
|
+
class MethodMissingRedirector
|
11
|
+
def initialize(&block)
|
12
|
+
@block = block
|
13
|
+
end
|
14
|
+
|
15
|
+
def method_missing(*args)
|
16
|
+
@block.call(*args)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def future(meth = nil, *args)
|
21
|
+
if meth
|
22
|
+
do_call(:future, meth, args)
|
23
|
+
else
|
24
|
+
MethodMissingRedirector.new do |method, *args|
|
25
|
+
do_call(:future, method, args)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def async(meth = nil, *args)
|
31
|
+
if meth
|
32
|
+
do_call(:async, meth, args)
|
33
|
+
else
|
34
|
+
MethodMissingRedirector.new do |method, *args|
|
35
|
+
do_call(:async, method, args)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def method_missing(meth, *args, &block)
|
41
|
+
# bang methods are async calls
|
42
|
+
type = :sync
|
43
|
+
if meth.match(/!$/)
|
44
|
+
meth = meth.to_s
|
45
|
+
meth.slice!(-1, 1)
|
46
|
+
type = :async
|
47
|
+
end
|
48
|
+
|
49
|
+
do_call(type, meth, args)
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
def do_call(type, meth, args)
|
54
|
+
case type
|
55
|
+
when :async then Celluloid::Actor.async(@mailbox, :async_remote_request, meth, *args)
|
56
|
+
when :sync then Celluloid::Actor.call(@mailbox, :sync_remote_request, meth, *args)
|
57
|
+
when :future then Celluloid::Actor.future(@mailbox, :sync_remote_request, meth, *args)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
class Client
|
64
|
+
include Celluloid
|
65
|
+
|
66
|
+
proxy_class ClientProxy
|
67
|
+
|
68
|
+
def initialize(endpoint, request_timeout = nil, reactor_name = :xs_reactor)
|
69
|
+
@reactor_name = reactor_name
|
70
|
+
@request_timeout = request_timeout
|
71
|
+
@socket = Actor[@reactor_name].socket(XS::XREQ)
|
72
|
+
rc = @socket.connect(endpoint)
|
73
|
+
unless XS::Util.resultcode_ok?(rc)
|
74
|
+
raise IOError, "connect failed: #{XS::Util.error_string}"
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
def sync_remote_request(method, *args)
|
80
|
+
Actor[@reactor_name].request(@socket, true, @request_timeout, method, *args)
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
def async_remote_request(method, *args)
|
85
|
+
Actor[@reactor_name].request(@socket, false, @request_timeout, method, *args)
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
@@ -0,0 +1,275 @@
|
|
1
|
+
require 'msgpack'
|
2
|
+
require 'celluloid'
|
3
|
+
require 'timers'
|
4
|
+
|
5
|
+
module ForeignActor
|
6
|
+
|
7
|
+
class InternalReactor
|
8
|
+
|
9
|
+
StopLoop = Class.new(RuntimeError)
|
10
|
+
|
11
|
+
def initialize(context = nil, poller = nil, timers = nil)
|
12
|
+
@context = context || XS::Context.new
|
13
|
+
@poller = poller || XS::Poller.new
|
14
|
+
@waiting_readables = {}
|
15
|
+
@servers = {}
|
16
|
+
@timers = timers || Timers.new
|
17
|
+
|
18
|
+
@control_socket_srv = @context.socket(XS::PAIR)
|
19
|
+
@control_socket_cl = @context.socket(XS::PAIR)
|
20
|
+
@control_messages = Array.new
|
21
|
+
|
22
|
+
addr = "inproc://ctrl"
|
23
|
+
@control_socket_srv.bind(addr)
|
24
|
+
@control_socket_cl.connect(addr)
|
25
|
+
|
26
|
+
@poller.register_readable(@control_socket_srv)
|
27
|
+
|
28
|
+
async(:run)
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
def suspend_reactor(msg = 'suspend')
|
33
|
+
if @control_socket_cl
|
34
|
+
@control_messages << msg
|
35
|
+
@control_socket_cl.send_string('')
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def wakeup_reactor()
|
40
|
+
signal(:resume_reactor)
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
def socket(*args)
|
45
|
+
@context.socket(*args)
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
def request(s, sync, timeout, method, *args)
|
50
|
+
type = sync ? 'sync_call' : 'async_call'
|
51
|
+
msg = build_msg(Celluloid::Task.current.object_id, type, method, args)
|
52
|
+
|
53
|
+
send_msg(s, '', msg)
|
54
|
+
wait_answer(s, timeout) if sync
|
55
|
+
end
|
56
|
+
|
57
|
+
# wait until the socket receives a new message
|
58
|
+
def wait_answer(s, timeout = nil)
|
59
|
+
task = Celluloid::Task.current
|
60
|
+
task_id = task.object_id
|
61
|
+
|
62
|
+
if @waiting_readables.has_key?(task_id)
|
63
|
+
raise ArgumentError, "task is already listening ???"
|
64
|
+
end
|
65
|
+
|
66
|
+
@poller.register_readable(s)
|
67
|
+
@waiting_readables[s] ||= {}
|
68
|
+
@waiting_readables[s][task_id] = task
|
69
|
+
|
70
|
+
timer = nil
|
71
|
+
if timeout
|
72
|
+
timer = @timers.after(timeout){ task.resume(:timeout) }
|
73
|
+
end
|
74
|
+
|
75
|
+
Celluloid::Task.suspend(:xs_wait).tap do
|
76
|
+
timer.cancel() if timer
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def serve_actor(endpoint, actor_or_name)
|
81
|
+
s = socket(XS::XREP)
|
82
|
+
rc = s.connect(endpoint)
|
83
|
+
unless XS::Util.resultcode_ok?(rc)
|
84
|
+
raise IOError, "connect failed: #{XS::Util.error_string}"
|
85
|
+
end
|
86
|
+
|
87
|
+
if @servers.has_key?(s)
|
88
|
+
raise ArgumentError, "another class is already registered as server for #{s}"
|
89
|
+
end
|
90
|
+
|
91
|
+
register_server(s, actor_or_name)
|
92
|
+
@poller.register_readable(s)
|
93
|
+
end
|
94
|
+
|
95
|
+
def run
|
96
|
+
loop { run_once() }
|
97
|
+
rescue StopLoop
|
98
|
+
|
99
|
+
end
|
100
|
+
|
101
|
+
def run_once(allow_blocking = true)
|
102
|
+
@timers.fire()
|
103
|
+
|
104
|
+
wait_time = if @timers.wait_interval
|
105
|
+
@timers.wait_interval * 1000
|
106
|
+
else
|
107
|
+
allow_blocking ? :blocking : 0
|
108
|
+
end
|
109
|
+
|
110
|
+
rc = @poller.poll(wait_time)
|
111
|
+
unless XS::Util.resultcode_ok?(rc)
|
112
|
+
raise IOError, "libxs poll error: #{XS::Util.error_string}"
|
113
|
+
end
|
114
|
+
|
115
|
+
@poller.readables.each do |s|
|
116
|
+
parts = receive_msg(s)
|
117
|
+
handle_message(s, parts)
|
118
|
+
end
|
119
|
+
|
120
|
+
rc
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
def build_response(src_msg, ret)
|
125
|
+
Serializer.pack(
|
126
|
+
task_id: src_msg['task_id'],
|
127
|
+
type: 'response',
|
128
|
+
ret: ret
|
129
|
+
)
|
130
|
+
end
|
131
|
+
|
132
|
+
def build_msg(task_id, type, method, args)
|
133
|
+
Serializer.pack(
|
134
|
+
task_id: task_id,
|
135
|
+
type: type,
|
136
|
+
method: method,
|
137
|
+
arguments: args
|
138
|
+
)
|
139
|
+
end
|
140
|
+
|
141
|
+
def register_server(s, actor_or_name)
|
142
|
+
@servers[s] = actor_or_name
|
143
|
+
end
|
144
|
+
|
145
|
+
|
146
|
+
def control_socket?(s)
|
147
|
+
s == @control_socket_srv
|
148
|
+
end
|
149
|
+
|
150
|
+
def handle_message(s, parts)
|
151
|
+
# control messages
|
152
|
+
if control_socket?(s)
|
153
|
+
ctrl_msg = @control_messages.shift
|
154
|
+
if ctrl_msg == 'exit'
|
155
|
+
puts "Exiting..."
|
156
|
+
signal(:exit_confirmed)
|
157
|
+
raise StopLoop
|
158
|
+
|
159
|
+
elsif ctrl_msg == 'suspend'
|
160
|
+
# allow the celluloid reactor to
|
161
|
+
# process its own waiting messages
|
162
|
+
wait(:resume_reactor)
|
163
|
+
|
164
|
+
# clear any suspend messages waiting to
|
165
|
+
# preventing sspending te reactor when nothing
|
166
|
+
# is actually expecting to (and as a result nothing
|
167
|
+
# will ever resume it).
|
168
|
+
@control_messages.reject!{|m| m == 'suspend' }
|
169
|
+
end
|
170
|
+
|
171
|
+
else
|
172
|
+
# or regular messages
|
173
|
+
|
174
|
+
# split the routing ids from the message
|
175
|
+
separator_index = parts.index('')
|
176
|
+
p parts unless separator_index
|
177
|
+
routing_ids = parts.slice!(0..separator_index)
|
178
|
+
|
179
|
+
msg = MessagePack.unpack(parts[0])
|
180
|
+
|
181
|
+
type = msg.delete('type')
|
182
|
+
case type
|
183
|
+
when 'sync_call'
|
184
|
+
# call it locally and then send the result back
|
185
|
+
handle_call(s, msg, routing_ids: routing_ids)
|
186
|
+
|
187
|
+
when 'async_call'
|
188
|
+
handle_call(s, msg)
|
189
|
+
|
190
|
+
when 'response'
|
191
|
+
if @waiting_readables[s]
|
192
|
+
task_id = msg['task_id']
|
193
|
+
task = @waiting_readables[s].delete(task_id)
|
194
|
+
if task && task.running?
|
195
|
+
task.resume(msg['ret'])
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
else
|
200
|
+
puts "unknown type: #{type}"
|
201
|
+
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
|
207
|
+
def handle_call(socket, msg, opts = {})
|
208
|
+
routing_ids = opts.delete(:routing_ids)
|
209
|
+
raise "unknown options: #{opts}" unless opts.empty?
|
210
|
+
|
211
|
+
server = @servers[socket]
|
212
|
+
unless server
|
213
|
+
raise ArgumentError, "no server registered for #{socket}"
|
214
|
+
end
|
215
|
+
|
216
|
+
if server.is_a?(Symbol)
|
217
|
+
server = Actor[server]
|
218
|
+
end
|
219
|
+
|
220
|
+
method, args = msg.values_at('method', 'arguments')
|
221
|
+
|
222
|
+
unless server
|
223
|
+
raise ArgumentError, "no server found for #{method}(#{args.join(', ')})"
|
224
|
+
end
|
225
|
+
|
226
|
+
# if we have the source id send the response, otherwise
|
227
|
+
# discards it.
|
228
|
+
if routing_ids
|
229
|
+
ret = server.send(method, *args)
|
230
|
+
msg = build_response(msg, ret)
|
231
|
+
send_msg(socket, *routing_ids, msg)
|
232
|
+
else
|
233
|
+
server.async(method, *args)
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
def receive_msg(s)
|
238
|
+
parts = []
|
239
|
+
loop do
|
240
|
+
data = ""
|
241
|
+
if s.recv_string(data, XS::DONTWAIT) == -1
|
242
|
+
raise "error while reading socket: #{}"
|
243
|
+
end
|
244
|
+
parts << data
|
245
|
+
break unless s.more_parts?
|
246
|
+
end
|
247
|
+
|
248
|
+
parts
|
249
|
+
end
|
250
|
+
|
251
|
+
def send_msg(s, *parts)
|
252
|
+
parts[0...-1].each do |m|
|
253
|
+
handle_xs_err(s, :send_string, m, XS::DONTWAIT | XS::SNDMORE)
|
254
|
+
end
|
255
|
+
|
256
|
+
handle_xs_err(s, :send_string, parts[-1], XS::DONTWAIT)
|
257
|
+
end
|
258
|
+
|
259
|
+
def handle_xs_err(s, method, *args)
|
260
|
+
rc = s.send(method, *args)
|
261
|
+
unless XS::Util.resultcode_ok?(rc)
|
262
|
+
raise "error: #{method}: #{XS::Util.error_string}"
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
end
|
267
|
+
|
268
|
+
class Reactor < InternalReactor
|
269
|
+
include Celluloid
|
270
|
+
mailbox_class ReactorMailbox
|
271
|
+
|
272
|
+
end
|
273
|
+
|
274
|
+
|
275
|
+
end
|