foreign_actor 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/docs/design.png ADDED
Binary file
data/example/Procfile ADDED
@@ -0,0 +1,2 @@
1
+ master: ruby ../bin/device
2
+ worker: ruby worker.rb
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
@@ -0,0 +1,10 @@
1
+
2
+ require 'rubygems'
3
+ require 'bundler/setup'
4
+
5
+ require 'foreign_actor'
6
+
7
+ $stdout.sync = true
8
+
9
+ CLIENT_ENDPOINT = 'tcp://127.0.0.1:7000'
10
+ WORKERS_ENDPOINT = 'tcp://127.0.0.1:7001'
@@ -0,0 +1,7 @@
1
+ endpoint1:
2
+ front: 'tcp://*:7000'
3
+ back: 'tcp://*:7001'
4
+
5
+ endpoint2:
6
+ front: 'tcp://*:7002'
7
+ back: 'tcp://*:7003'
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
@@ -0,0 +1,2 @@
1
+ master: ruby ../bin/device
2
+ node: ruby node.rb
@@ -0,0 +1,9 @@
1
+
2
+ In this example each node is both a client and a server, the more nodes you start,
3
+ the quicker their tasks will complete. If a node is killed the job will timeout.
4
+
5
+ Just run:
6
+ $ ruby ../bin/device
7
+
8
+ And then in multiple consoles:
9
+ $ ruby node.rb
@@ -0,0 +1,4 @@
1
+ endpoint1:
2
+ front: 'tcp://*:7000'
3
+ back: 'tcp://*:7001'
4
+
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