zactor 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,71 @@
1
+ module Zactor
2
+ class BrokerIn
3
+ attr_accessor :broker, :params
4
+ def initialize(broker, params = {})
5
+ @broker = broker
6
+ @params = params
7
+ init_connection
8
+ end
9
+
10
+ def on_readable(socket, messages)
11
+ Zactor.logger.debug "Broker: messages"
12
+ @broker.pub.request messages
13
+ end
14
+
15
+ def close
16
+ @connection.unbind
17
+ rescue
18
+ end
19
+ end
20
+
21
+ class BrokerPub
22
+ include ZMQMEssages
23
+ def initialize(broker)
24
+ @broker = broker
25
+ @connection = Zactor.zmq.bind ZMQ::PUB, "inproc://zactor_broker_pub", self
26
+ @socket = @connection.socket
27
+ end
28
+
29
+ def request(messages)
30
+ Zactor.logger.debug { "Broker: request #{messages.map(&:copy_out_string).inspect}" }
31
+ send_messages messages
32
+ end
33
+
34
+ def close
35
+ @connection.unbind
36
+ rescue
37
+ end
38
+ end
39
+
40
+ class BrokerSub < BrokerIn
41
+ def init_connection
42
+ Zactor.logger.info "Starting sub broker tcp://0.0.0.0:#{Zactor.broker_port}"
43
+ @connection = Zactor.zmq.bind ZMQ::SUB, "tcp://0.0.0.0:#{Zactor.broker_port}", self
44
+ @connection.bind "inproc://zactor_broker_sub"
45
+ @connection.subscribe ''
46
+ end
47
+ end
48
+
49
+ class BrokerPull < BrokerIn
50
+ def init_connection
51
+ Zactor.logger.info "Starting pull broker tcp://#{params[:host]}"
52
+ @connection = Zactor.zmq.connect ZMQ::PULL, "tcp://#{params[:host]}", self
53
+ end
54
+ end
55
+
56
+ class Broker
57
+ attr_accessor :sub, :pub
58
+ def initialize(params = {})
59
+ Zactor.logger.info "Broker: starting"
60
+ @pub = BrokerPub.new self
61
+ @subs = []
62
+ @subs << BrokerSub.new(self)
63
+ @subs << BrokerPull.new(self, :host => params[:balancer]) if params[:balancer]
64
+ end
65
+
66
+ def finish
67
+ @pub.close
68
+ @subs.each(&:close)
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,25 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'active_support/log_subscriber'
3
+ module Zactor
4
+ class LogSubscriber < ActiveSupport::LogSubscriber
5
+ def logger
6
+ Zactor.logger
7
+ end
8
+
9
+ def merge(event)
10
+ payload = event.payload
11
+
12
+ message = "[Zactor](%.0fms) sending request '#{payload[:event]}' to '#{payload[:actor]}'' with params #{paylaod[:args].inspect}" % event.duration
13
+ debug message
14
+ end
15
+
16
+ def send_to(event)
17
+ payload = event.payload
18
+
19
+ message = "[Zactor] sending messages to '#{payload[:actor]}'" % event.duration
20
+ debug message
21
+ end
22
+ end
23
+ end
24
+
25
+ Zactor::LogSubscriber.attach_to :zactor
@@ -0,0 +1,23 @@
1
+ module Zactor
2
+ class Message
3
+ attr_accessor :actor, :params
4
+ attr_accessor :callback_id, :args
5
+ def initialize(actor, params = {})
6
+ @actor = actor
7
+ @params = params
8
+ end
9
+
10
+ def sender
11
+ params[:sender]
12
+ end
13
+
14
+ def args
15
+ params[:args]
16
+ end
17
+
18
+ def reply(*args)
19
+ return false unless params[:callback_id]
20
+ actor.send_reply sender, params[:callback_id], *args
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,3 @@
1
+ module Zactor
2
+ VERSION = "0.0.4"
3
+ end
data/lib/zactor.rb ADDED
@@ -0,0 +1,263 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'ruby_interface'
3
+ require 'active_support/all'
4
+ require 'ffi-rzmq'
5
+ require 'em-zeromq'
6
+ require 'bson'
7
+
8
+ module Zactor
9
+ extend ActiveSupport::Autoload
10
+
11
+ autoload :Broker
12
+ autoload :ActorSub
13
+ autoload :ActorPub
14
+ autoload :Message
15
+ require 'zactor/log_subscriber'
16
+
17
+ mattr_accessor :stub
18
+ mattr_accessor :zmq, :logger
19
+
20
+ class << self
21
+ def broker
22
+ @broker
23
+ end
24
+
25
+ def broker_port
26
+ @broker_port
27
+ end
28
+
29
+ def host
30
+ @host
31
+ end
32
+
33
+ def start(broker_port, params = {})
34
+ self.zmq ||= EM::ZeroMQ::Context.new(1)
35
+ self.logger ||= Logger.new(STDOUT).tap { |o| o.level = params[:debug] ? Logger::DEBUG : Logger::INFO }
36
+ @host = "#{params.delete(:host) || '0.0.0.0'}:#{broker_port}"
37
+ @broker_port = broker_port
38
+ @params = params
39
+
40
+ logger.info "Starting Zactor"
41
+ @broker = Broker.new :balancer => params[:balancer]
42
+ end
43
+
44
+ def get_actor(pid, options = {})
45
+ h = options[:host] || host
46
+ { 'identity' => pid, 'host' => h }
47
+ end
48
+
49
+ def register(zactor)
50
+ @zactors ||= {}
51
+ @zactors[zactor] = true
52
+ end
53
+
54
+ def deregister(zactor)
55
+ @zactors.delete zactor
56
+ end
57
+
58
+ def finish
59
+ @zactors.keys.each(&:finish)
60
+ @broker.finish
61
+ end
62
+
63
+ def clear
64
+ @zactors = {}
65
+ end
66
+ end
67
+
68
+
69
+ module ZMQMEssages
70
+ class ZMQMessage
71
+ attr_accessor :res
72
+ def initialize(&blk)
73
+ @res = []
74
+ blk.call self
75
+ end
76
+
77
+ def str(str)
78
+ @res << ZMQ::Message.new.tap { |o| o.copy_in_string str.to_s }
79
+ end
80
+
81
+ def mes(message)
82
+ @res << message
83
+ end
84
+ end
85
+
86
+ def send_messages(messages)
87
+ last = messages[-1]
88
+ messages[0...-1].each do |mes|
89
+ sent = @socket.send mes, ZMQ::NOBLOCK | ZMQ::SNDMORE
90
+ break unless sent
91
+ end
92
+ sent = @socket.send last, ZMQ::NOBLOCK
93
+ unless sent
94
+ Zactor.logger.info "[Zactor] Error while sending messages"
95
+ end
96
+ end
97
+
98
+ def messages(&blk)
99
+ ZMQMessage.new(&blk).res
100
+ end
101
+ end
102
+
103
+
104
+ extend RubyInterface
105
+ interface :zactor do
106
+ interfaced do
107
+ self.zactor do
108
+ event(:finish) { |o| o.finish }
109
+ event(:link) do |o, msg|
110
+ o.zactor.linked msg
111
+ # msg.reply :ok
112
+ end
113
+ event(:link_ping) do |o, msg|
114
+ msg.reply :pong
115
+ end
116
+ end
117
+ end
118
+
119
+ include ZMQMEssages
120
+
121
+ class_attribute :identity_val
122
+ class_attribute :events
123
+ self.events = {}
124
+ class << self
125
+ def identity(val)
126
+ self.identity_val = val
127
+ end
128
+
129
+ def event(name, &clb)
130
+ self.events = self.events.merge name.to_sym => clb
131
+ end
132
+ end
133
+ attr_accessor :actor, :identity
134
+ attr_accessor :pubs
135
+
136
+ # Инициализация, подписывается на сообщения для себя, создает сокет для отправки локальных сообщений
137
+ def init
138
+ @actor = Zactor.get_actor @identity || self.class.identity_val || "actor.#{owner.object_id}-#{Zactor.host}"
139
+ Zactor.register self
140
+ return if Zactor.stub
141
+ @sub = make_sub
142
+ @callbacks, @timeouts = {}, {}
143
+ @pubs = {}
144
+ @pubs["0.0.0.0:#{Zactor.broker_port}"] = make_pub "inproc://zactor_broker_sub"
145
+ end
146
+
147
+ # Закрываем все соедниения и чистим сисьему. Обязательно нужно делать, когда объект перестает существовать
148
+ def finish
149
+ Zactor.deregister self
150
+ return if Zactor.stub
151
+ if @linked
152
+ @linked.each do |link|
153
+ link.reply :finish
154
+ end
155
+ end
156
+ @finished = true
157
+ @sub.close
158
+ @pubs.values.each(&:close)
159
+ end
160
+
161
+ def make_pub(endpoint)
162
+ Zactor::ActorPub.new self, endpoint
163
+ end
164
+
165
+ def make_sub
166
+ Zactor::ActorSub.new self
167
+ end
168
+
169
+ def send_request(actor, event, *args, &clb)
170
+ return if @finished || Zactor.stub
171
+ @callbacks[clb.object_id.to_s] = clb if clb
172
+ send_to actor, messages { |m|
173
+ m.str 'request'
174
+ m.str "#{clb ? clb.object_id : ''}"
175
+ m.str event
176
+ m.str BSON.serialize({ 'args' => args })
177
+ }
178
+ @last_callback = clb.object_id.to_s
179
+ self
180
+ end
181
+
182
+ def timeout(secs, &clb)
183
+ raise "Only for requests" unless @last_callback
184
+ @timeouts[@last_callback] = EM.add_timer(secs, &clb)
185
+ end
186
+
187
+ def send_reply(actor, callback_id, *args)
188
+ return if @finished || Zactor.stub
189
+ Zactor.logger.debug "Zactor: send reply"
190
+ send_to actor, messages { |m|
191
+ m.str 'reply'
192
+ m.str callback_id
193
+ m.str BSON.serialize({ 'args' => args })
194
+ }
195
+ end
196
+
197
+ def send_to(actor, mes = [])
198
+ pub = @pubs[actor['host']] ||= make_pub("tcp://#{actor['host']}")
199
+ pub.send_messages(messages { |m|
200
+ m.str actor['identity']
201
+ m.str bson_actor
202
+ } + mes)
203
+ end
204
+
205
+ def link(actor, &clb)
206
+ Zactor.logger.debug { "Zactor: link #{actor} with #{self.actor}"}
207
+ send_request actor, :link do
208
+ clb.call
209
+ end
210
+ link_ping actor, &clb
211
+ self
212
+ end
213
+
214
+ def link_ping(actor, &clb)
215
+ EM.add_timer(5) do
216
+ unless @finished
217
+ send_request(actor, :link_ping) do
218
+ link_ping actor, &clb
219
+ end.timeout(5) do
220
+ clb.call
221
+ end
222
+ end
223
+ end
224
+ end
225
+
226
+ def linked(msg)
227
+ Zactor.logger.debug { "Zactor: linked #{actor} with #{msg.sender}"}
228
+ @linked ||= []
229
+ @linked << msg
230
+ end
231
+
232
+ def receive_reply(callback_id, *args)
233
+ Zactor.logger.debug "Zactor: receive reply"
234
+ if (callback = @callbacks[callback_id])
235
+ if timeout = @timeouts[callback_id]
236
+ EM.cancel_timer timeout
237
+ end
238
+ callback.call(*args)
239
+ end
240
+ end
241
+
242
+ def receive_request(sender, event_name, callback_id, *args)
243
+ Zactor.logger.debug "Zactor: receive request"
244
+ mes = Message.new self, :sender => sender, :callback_id => callback_id, :args => args
245
+ if (event = self.class.events[event_name.to_sym])
246
+ event.call owner, mes, *args
247
+ else
248
+ raise "Undefined event #{event_name}"
249
+ end
250
+ end
251
+
252
+ def bson_actor
253
+ @bson_actor ||= BSON.serialize(@actor).to_s
254
+ end
255
+
256
+
257
+ def inspect
258
+ "<#Zactor Interface for #{owner.inspect}"
259
+ end
260
+
261
+ end
262
+
263
+ end
@@ -0,0 +1,136 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require 'spec_helper'
4
+
5
+ describe "Zactor actor" do
6
+ class A
7
+ include Zactor
8
+ end
9
+ before do
10
+ stub(Zactor::Broker).new
11
+ Zactor.start 8000
12
+ end
13
+ describe "init" do
14
+ let(:actor) { A.new.zactor }
15
+ let(:sub) { Object.new }
16
+ let(:local_pub) { Object.new }
17
+ before do
18
+ stub(actor).make_sub { sub }
19
+ stub(actor).make_pub { local_pub }
20
+ end
21
+ it "должен зарегистрировать себя" do
22
+ mock(Zactor).register actor
23
+ actor.init
24
+ end
25
+
26
+ it "должен создать sub сокет" do
27
+ mock(actor).make_sub { sub }
28
+ actor.init
29
+ actor.instance_eval { @sub }.should eq(sub)
30
+ end
31
+
32
+ it "должен создать pub сокет для локальных вызовов" do
33
+ mock(actor).make_pub("inproc://zactor_broker_sub") { local_pub }
34
+ actor.init
35
+ actor.instance_eval { @pubs['0.0.0.0:8000'] }.should eq(local_pub)
36
+ end
37
+
38
+ describe "identity" do
39
+ it "по-умолчанию" do
40
+ actor.init
41
+ actor.actor.should eq({ 'identity' => "actor.#{actor.owner.object_id}-0.0.0.0:8000", 'host' => '0.0.0.0:8000' })
42
+ end
43
+
44
+ it "с глобальным указанием" do
45
+ A.zactor.identity "a"
46
+ actor.init
47
+ actor.actor.should eq({ 'identity' => "a", 'host' => '0.0.0.0:8000' })
48
+ end
49
+
50
+ it "с указанием для этого объекта" do
51
+ actor.identity = "b"
52
+ actor.init
53
+ actor.actor.should eq({ 'identity' => "b", 'host' => '0.0.0.0:8000' })
54
+ end
55
+ end
56
+
57
+ describe "after init" do
58
+ include Zactor::ZMQMEssages
59
+
60
+ def assert_messages(got, expected)
61
+ got.map(&:copy_out_string).should eq(expected.map(&:copy_out_string))
62
+ end
63
+
64
+ before do
65
+ actor.init
66
+ end
67
+ describe "send_to" do
68
+ it "должен отправить в локальный pub для локального объекта" do
69
+ mes = messages { |m| }
70
+ stub(local_pub).send_messages do |mes|
71
+ assert_messages mes, messages { |m|
72
+ m.str('b')
73
+ m.str(actor.bson_actor)
74
+ }
75
+ end
76
+ actor.send_to Zactor.get_actor('b')
77
+ end
78
+
79
+ it "должен отправить в удаленный pub для удаленного объекта, перед этим создав его" do
80
+ remote_pub = Object.new
81
+ mock(actor).make_pub("tcp://192.168.1.1:3000") { remote_pub }
82
+ stub(remote_pub).send_messages do |mes|
83
+ assert_messages mes, messages { |m| m.str('b'); m.str(actor.bson_actor) }
84
+ end
85
+ actor.send_to Zactor.get_actor('b', :host => '192.168.1.1:3000')
86
+ end
87
+ end
88
+
89
+ describe "send_request" do
90
+ it "должен отправлять сообщение типа request" do
91
+ stub(local_pub).send_messages do |mes|
92
+ assert_messages mes, messages { |m|
93
+ m.str('b')
94
+ m.str(actor.bson_actor)
95
+
96
+ m.str 'request'
97
+ m.str ""
98
+ m.str "show"
99
+ m.str BSON.serialize({ 'args' => ['foo', :bar] })
100
+ }
101
+ end
102
+ actor.send_request Zactor.get_actor('b'), :show, 'foo', :bar
103
+ end
104
+ end
105
+
106
+ describe "send_reply" do
107
+ it "должен отправлять сообщение типа reply" do
108
+ stub(local_pub).send_messages do |mes|
109
+ assert_messages mes, messages { |m|
110
+ m.str('b')
111
+ m.str(actor.bson_actor)
112
+
113
+ m.str 'reply'
114
+ m.str 5
115
+ m.str BSON.serialize({ 'args' => ['foo', :bar] })
116
+ }
117
+ end
118
+ actor.send_reply Zactor.get_actor('b'), 5, 'foo', :bar
119
+ end
120
+ end
121
+
122
+ describe "receive_reply" do
123
+
124
+ end
125
+
126
+ describe "receive_request" do
127
+
128
+ end
129
+
130
+ describe "finish" do
131
+
132
+ end
133
+ end
134
+
135
+ end
136
+ end
@@ -0,0 +1,81 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require 'spec_helper'
4
+ require "em-spec/rspec"
5
+
6
+ describe "Zactor" do
7
+ include EM::SpecHelper
8
+ module Exchange
9
+ class A
10
+ include Zactor
11
+
12
+ def initialize
13
+ zactor.init
14
+ end
15
+
16
+ def ping(actor = Zactor.get_actor("b"))
17
+ zactor.send_request actor, :ping do |res|
18
+ reply
19
+ end
20
+ end
21
+
22
+ def reply
23
+
24
+ end
25
+ end
26
+
27
+ class B
28
+ include Zactor
29
+
30
+ zactor do
31
+ identity "b"
32
+
33
+ event(:ping) do |o, msg|
34
+ o.receive
35
+ msg.reply "Pong!"
36
+ end
37
+ end
38
+
39
+ def initialize
40
+ zactor.init
41
+ end
42
+
43
+ def receive
44
+
45
+ end
46
+ end
47
+ end
48
+
49
+ after do
50
+ Zactor.clear
51
+ Zactor.finish
52
+ end
53
+
54
+ it "B должен получить сообщение" do
55
+ em do
56
+ Zactor.start 8000, :debug => true
57
+ Exchange::A.new.ping
58
+ b = Exchange::B.new
59
+ mock.proxy(b).receive { done }
60
+ end
61
+ end
62
+
63
+ it "A должен получить ответ" do
64
+ em do
65
+ Zactor.start 8000, :debug => true
66
+ a = Exchange::A.new
67
+ a.ping
68
+ Exchange::B.new
69
+ mock.proxy(a).reply { done }
70
+ end
71
+ end
72
+
73
+ it "Если задан таймаут, то он должен срабатывать" do
74
+ em(7) do
75
+ Zactor.start 8000, :debug => true
76
+ a = Exchange::A.new.ping.timeout(5) do
77
+ done
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,67 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require 'spec_helper'
4
+ require "em-spec/rspec"
5
+
6
+ describe "Zactor" do
7
+ include EM::SpecHelper
8
+ module Link
9
+ class A
10
+ include Zactor
11
+
12
+ def initialize
13
+ zactor.init
14
+ end
15
+ end
16
+
17
+ class B
18
+ include Zactor
19
+
20
+ def initialize
21
+ zactor.init
22
+ end
23
+ end
24
+ end
25
+
26
+ after do
27
+ puts "FINISH ZACTOR"
28
+ Zactor.finish
29
+ end
30
+
31
+ it "A должен уведомляться о смерти B" do
32
+ em do
33
+ Zactor.start 8000, :debug => true
34
+ a = Link::A.new
35
+ b = Link::B.new
36
+ a.zactor.link b.zactor.actor do
37
+ done
38
+ end
39
+ EM.add_timer(0.5) do
40
+ b.zactor.finish
41
+ end
42
+ end
43
+ end
44
+
45
+ # it "A должен уведомляться о смерти B в случае удаления объекта гарбэйдж коллектором" do
46
+ #
47
+ # end
48
+
49
+ it "A должен уведомляться о смерти B, даже если B закончил выполнение неожиданно" do
50
+ em(12) do
51
+ EM.add_timer(0.5) do #FIXME ZMQ-сокетам нужно время чтобы закрыться. Нужно придумать, что с этим делать
52
+ Zactor.start 8000, :debug => true
53
+ a = Link::A.new
54
+ b = Link::B.new
55
+ a.zactor.link b.zactor.actor do
56
+ done
57
+ end
58
+ EM.add_timer(0.5) do
59
+ b.zactor.instance_eval do
60
+ @linked = []
61
+ end
62
+ b.zactor.finish
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,40 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require 'spec_helper'
4
+
5
+ describe "Zactor" do
6
+ describe "start" do
7
+ before do
8
+ stub(Zactor::Broker).new
9
+ end
10
+ it "должен выставлять хост по-умолчанию 0.0.0.0" do
11
+ Zactor.start 8000
12
+ Zactor.host.should eq('0.0.0.0:8000')
13
+ end
14
+
15
+ it "должен выставлять хост с учетом переданного" do
16
+ Zactor.start 8000, :host => '192.168.1.1'
17
+ Zactor.host.should eq('192.168.1.1:8000')
18
+ end
19
+
20
+ it "должен создавать новый брокер с указаным балансером" do
21
+ mock(Zactor::Broker).new :balancer => '0.0.0.0:4000'
22
+ Zactor.start 8000, :balancer => '0.0.0.0:4000'
23
+ end
24
+ end
25
+
26
+ describe "get_actor" do
27
+ before do
28
+ stub(Zactor::Broker).new
29
+ Zactor.start 8000
30
+ end
31
+ it "в качестве хоста по-умолчанию ставит себя же" do
32
+ Zactor.get_actor("actor1").should eq({ 'identity' => 'actor1', 'host' => '0.0.0.0:8000' })
33
+ end
34
+
35
+
36
+ it "в качестве хоста ставит переданный" do
37
+ Zactor.get_actor("actor1", :host => '192.168.1.1:3000').should eq({ 'identity' => 'actor1', 'host' => '192.168.1.1:3000' })
38
+ end
39
+ end
40
+ end