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/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