zactor 0.0.5 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
data/.rake_tasks~ ADDED
@@ -0,0 +1,19 @@
1
+ build
2
+ console[script]
3
+ gemcutter:release
4
+ gemspec
5
+ gemspec:debug
6
+ gemspec:generate
7
+ gemspec:release
8
+ gemspec:validate
9
+ git:release
10
+ install
11
+ rcov
12
+ release
13
+ spec
14
+ version
15
+ version:bump:major
16
+ version:bump:minor
17
+ version:bump:patch
18
+ version:write
19
+ yard
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ -m markdown -e ruby-interface/yard
data/Gemfile CHANGED
@@ -1,13 +1,17 @@
1
1
  source "http://rubygems.org"
2
2
 
3
3
  group :development do
4
- gem "rspec", "~> 2.3.0"
4
+ gem "rspec", ">= 2"
5
5
  gem "yard", "~> 0.6.0"
6
6
  gem "bundler", "~> 1.0.0"
7
- gem "jeweler", "~> 1.5.2"
8
7
  gem "rcov", ">= 0"
9
8
  gem "bluecloth"
10
9
  gem "rr"
10
+ gem "guard-rspec"
11
+ gem "growl"
12
+ gem "ruby-debug19"
13
+ gem "em-spec", :git => "https://github.com/mloughran/em-spec.git", :branch => 'rspec2'
11
14
  end
12
15
 
16
+
13
17
  gemspec
data/Gemfile.lock CHANGED
@@ -1,33 +1,80 @@
1
+ GIT
2
+ remote: https://github.com/mloughran/em-spec.git
3
+ revision: fc888b289a424fa93848ffdac3dc4b24fdd34950
4
+ branch: rspec2
5
+ specs:
6
+ em-spec (0.2.2)
7
+
1
8
  PATH
2
9
  remote: .
3
10
  specs:
4
- zactor (0.0.5)
11
+ zactor (0.0.6)
5
12
  activesupport (> 0.1)
6
- i18n (> 0.1)
13
+ bson (> 0.1)
14
+ bson_ext (> 0.1)
15
+ em-zeromq (> 0.1)
16
+ ffi (> 0.1)
17
+ ffi-rzmq (> 0.1)
18
+ ruby-interface (> 0)
7
19
 
8
20
  GEM
9
21
  remote: http://rubygems.org/
10
22
  specs:
11
23
  activesupport (3.0.7)
24
+ archive-tar-minitar (0.5.2)
12
25
  bluecloth (2.1.0)
26
+ bson (1.2.4)
27
+ bson_ext (1.2.4)
28
+ columnize (0.3.2)
29
+ configuration (1.2.0)
13
30
  diff-lcs (1.1.2)
14
- git (1.2.5)
31
+ em-zeromq (0.2.1)
32
+ eventmachine (>= 1.0.0.beta.3)
33
+ ffi-rzmq (>= 0.7.2)
34
+ eventmachine (1.0.0.beta.3)
35
+ ffi (1.0.7)
36
+ rake (>= 0.8.7)
37
+ ffi-rzmq (0.7.2)
38
+ growl (1.0.3)
39
+ guard (0.3.0)
40
+ open_gem (~> 1.4.2)
41
+ thor (~> 0.14.6)
42
+ guard-rspec (0.2.0)
43
+ guard (>= 0.2.2)
15
44
  i18n (0.5.0)
16
- jeweler (1.5.2)
17
- bundler (~> 1.0.0)
18
- git (>= 1.2.5)
19
- rake
45
+ launchy (0.3.7)
46
+ configuration (>= 0.0.5)
47
+ rake (>= 0.8.1)
48
+ linecache19 (0.5.11)
49
+ ruby_core_source (>= 0.1.4)
50
+ open_gem (1.4.2)
51
+ launchy (~> 0.3.5)
20
52
  rake (0.8.7)
21
53
  rcov (0.9.9)
22
54
  rr (1.0.2)
23
- rspec (2.3.0)
24
- rspec-core (~> 2.3.0)
25
- rspec-expectations (~> 2.3.0)
26
- rspec-mocks (~> 2.3.0)
27
- rspec-core (2.3.1)
28
- rspec-expectations (2.3.0)
55
+ rspec (2.5.0)
56
+ rspec-core (~> 2.5.0)
57
+ rspec-expectations (~> 2.5.0)
58
+ rspec-mocks (~> 2.5.0)
59
+ rspec-core (2.5.1)
60
+ rspec-expectations (2.5.0)
29
61
  diff-lcs (~> 1.1.2)
30
- rspec-mocks (2.3.0)
62
+ rspec-mocks (2.5.0)
63
+ ruby-debug-base19 (0.11.24)
64
+ columnize (>= 0.3.1)
65
+ linecache19 (>= 0.5.11)
66
+ ruby_core_source (>= 0.1.4)
67
+ ruby-debug19 (0.11.6)
68
+ columnize (>= 0.3.1)
69
+ linecache19 (>= 0.5.11)
70
+ ruby-debug-base19 (>= 0.11.19)
71
+ ruby-interface (0.0.2)
72
+ activesupport (> 0.1)
73
+ i18n (> 0.1)
74
+ ruby-interface
75
+ ruby_core_source (0.1.4)
76
+ archive-tar-minitar (>= 0.5.2)
77
+ thor (0.14.6)
31
78
  yard (0.6.5)
32
79
 
33
80
  PLATFORMS
@@ -36,9 +83,12 @@ PLATFORMS
36
83
  DEPENDENCIES
37
84
  bluecloth
38
85
  bundler (~> 1.0.0)
39
- jeweler (~> 1.5.2)
86
+ em-spec!
87
+ growl
88
+ guard-rspec
40
89
  rcov
41
90
  rr
42
- rspec (~> 2.3.0)
91
+ rspec (>= 2)
92
+ ruby-debug19
43
93
  yard (~> 0.6.0)
44
94
  zactor!
data/Guardfile ADDED
@@ -0,0 +1,12 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'rspec', :version => 2 do
5
+ watch(/^spec\/(.*)_spec.rb/)
6
+ watch(/^lib\/(.*)\.rb/) { |m| "spec/lib/#{m[1]}_spec.rb" }
7
+ watch(/^spec\/spec_helper.rb/) { "spec" }
8
+
9
+ # watch(/^lib\/zactor.rb/) { "spec/lib/actor_spec.rb" }
10
+
11
+ watch(/^app\/(.*)\.rb/) { |m| "spec/app/#{m[1]}_spec.rb" }
12
+ end
data/README.md CHANGED
@@ -1,93 +1,167 @@
1
- RubyInterface
2
- =============
1
+ Что это
2
+ =======
3
+ Zactor позволяет любой руби-объект научить общаться с другими объектами посредством отправки и получения сообщений. При этом неважно где именно расположен объект, в том же процессе, в соседнем, или на другой физической машине.
3
4
 
4
- Простенький патерн определения интерфейсов в руби. В противовес стандартным миксинам, для каждого интерфейса создается свой класс и соответсвенно экземпляр класса для каждого объекта с интерфейсом.
5
+ Zactor использует zmq как бекенд для сообщений, eventmachine и em-zeromq для асинхронной работы с zmq, RubyInterface для описания себя, BSON для сериализации данных.
5
6
 
6
- module Tree
7
- extend RubyInterface
8
- interface :tree do
9
- include Enumerable
10
- attr_accessor :parent
11
-
12
- def childs
13
- @childs ||= []
14
- end
15
-
16
- def each(&blk)
17
- blk.call(owner)
18
- childs.each { |v| v.tree.each(&blk) }
19
- end
20
-
21
- def set_parent(parent)
22
- parent.tree.childs << owner
23
- @parent = parent
24
- end
25
- end
26
- end
7
+ Каждый zactor-объект имеет свой identity, который генерируется автоматически, либо задается вручную и постоянен. Совокупность identity, host и port на которых был рожден этот объект, это достаточная информация для того что бы отправить этому объекту сообщение из другого объекта. Выглядит примерно так:
8
+
9
+ zactor.actor =>
10
+ {"identity"=>"actor.2154247120-0.0.0.0:8000", "host"=>"0.0.0.0:8000"}
11
+
12
+ Ипользование
13
+ ============
14
+
15
+ Для начала нужно стартануть Zactor.
16
+
17
+ Zactor.start 8000
18
+
19
+ Процесс забиндится на 0.0.0.0:8000, через эту точку будет происходить общение между zactor-процессами. Стартоваться должен в запущенном EM-контексте.
20
+
21
+ В каждый zactor-активный класс нужно делать include Zactor, после чего у класса и его экземпляров для доступа к функциями Zactor появится метод zactor. После создания объекта нужно выполнить zactor.init
27
22
 
28
23
  class A
29
- include Tree
24
+ include Zactor
25
+
26
+ def initialize
27
+ zactor.init
28
+ end
30
29
  end
30
+
31
+ Для отправки сообщений другому объекту нам нужно знать его идентификатор. Идентификатор можно получить тремя способами:
31
32
 
32
- При разработке интерфейса не нужно задумываться о конфликтах имен переменных, методов, можно делать все что угодно. Аргументом к методу interface передается название метода, по которому этот интерфейс будет доступен.
33
-
34
- a = A.new
35
- b = A.new
33
+ * Непосрдественной передачей. При инициализации или в любом другом месте, это исключительно внутренняя логика приложения. Идентификатор объекта можно получить вызвав zactor.actor
34
+ * При получении сообщения. В сообщении всегда содержится информация об отправителе
35
+ * Если объект имеет заранее известный identity, то мы можем получить его полный идентификатор вызвав Zactor.get_actor с identity и хостом, на котором он запущен
36
+
37
+ actor = Zactor.get_actor "broker", :host => "0.0.0.0:8001"
38
+
39
+ Получив идентификатор можно отправлять ему сообщения
40
+
41
+ zactor.send_request actor, :show_me, :boobs
36
42
 
37
- a.tree.set_parent b
38
- b.tree.childs # => [a]
39
- b.tree.map { |o| o } # => [b, a]
40
-
41
- А при использовании методов относящихся к интерфейсу мы явно видим к какому же интерфейсу он относится. Всем профит!
43
+
44
+ Каждый класс может определять какие именно ивенты он может получать и что с ними делать
45
+
46
+ include Zactor
42
47
 
43
- В интерфейсе доступен метод owner, возвращающий родительский объект. У класса интерфейса есть <tt>interface_base</tt>, возвращающий класс, куда интерфейс был заинклужен.
48
+ zactor do
49
+ event(:show_me) do |o, msg, what|
50
+ case what
51
+ when :boobs
52
+ do_it
53
+ else
54
+ do_smth_else
55
+ end
56
+ end
57
+ end
58
+
59
+ Рассмотрим пример банального ping-pong
44
60
 
45
- Помимо инстанс метода, создается так же класс-метод. В него можно передать блок, который выполнится в скоупе класса интерфейса. Сам метод возвращает класс интерфейса.
61
+ class A
62
+ include Zactor
46
63
 
47
- module StateMachine
48
- extend RubyInterface
49
- interface :state_machine do
50
- def self.state(name)
51
- puts "New state #{name}"
64
+ def initialize
65
+ zactor.init
66
+ ping Zactor.get_actor("b")
67
+ end
68
+
69
+ def ping(actor)
70
+ puts "Ping!"
71
+ zactor.send_request actor, :ping do |res|
72
+ puts res
52
73
  end
53
74
  end
54
75
  end
55
76
 
56
- class A
57
- include StateMachine
77
+
78
+ class B
79
+ include Zactor
80
+
81
+ zactor do
82
+ identity "b"
83
+
84
+ event(:ping) do |o, msg|
85
+ msg.reply "Pong!"
86
+ end
87
+ end
58
88
 
59
- state_machine do
60
- state(:parked) # => New state parked
61
- state(:idling) # => New state idling
89
+ def initialize
90
+ zactor.init
62
91
  end
92
+
93
+ end
94
+
95
+ EM.run do
96
+ Zactor.start 8000
97
+
98
+ a = A.new
99
+ b = B.new
63
100
  end
101
+
102
+ A посылает сообщение :ping для B, а B отвечает "Pong!"
103
+
104
+ В коллбэк определенный в event передается объект получившый сообщение, объект сообщения ({Zactor::Message}) и далее переданные в запросе аргументы (если они есть). У {Zactor::Message} есть два основных метода: sender, возвращающий идентификатор отправителя и reply, который посылает ответ на запрос.
105
+
106
+ Важный момент, identity должно задаваться ДО zactor.init и после этого не может меняться.
107
+
108
+ ZMQ
109
+ ===
110
+
111
+ При Zactor.start стартует брокер, по одному на каждый процесс, через него проходят все сообщения данного процесса, принимает сообщения через SUB-сокет, отправляет через PUB. SUB подписан на все сообщения. Каждый zactor-объект создает по паре сокетов, PUB подключается к SUB-брокера, а SUB к PUB-брокера. SUB подписывается на сообщения содержащие его identity.
112
+
113
+ ![ZMQ](images/zmq1.png)
114
+
115
+ Рассмотрим жизнь сообщения на примере с ping-ping. В случае с b в том же процессе:
116
+
117
+ <div class=wsd wsd_style="default"><pre>
118
+ A[PUB]->Broker[SUB]: Посылаем запрос :ping
119
+ Broker[SUB]->Broker[PUB]: Перебрасываем запрос в PUB сокет
120
+ Broker[PUB]->B[SUB]: Передаем получателю сообщение
121
+ B[PUB]->Broker[SUB]: Отправляем ответ "Pong!"
122
+ Broker[SUB]->Broker[PUB]: Перебрасываем запрос в PUB сокет
123
+ Broker[PUB]->A[SUB]: Отправитель получает ответ
124
+ </pre></div><script type="text/javascript" src="http://www.websequencediagrams.com/service.js"></script>
125
+
126
+ В случае с b в другом процессе:
127
+
128
+ <div class=wsd wsd_style="default"><pre>
129
+ A[PUB for App2]->App2 Broker[SUB]: Посылаем запрос :ping
130
+ App2 Broker[SUB]->App2 Broker[PUB]: Перебрасываем запрос в PUB сокет
131
+ App2 Broker[PUB]->B[SUB]: Передаем получателю сообщение
132
+ B[PUB for App1]->App1 Broker[SUB]: Отправляем ответ "Pong!"
133
+ App1 Broker[SUB]->App1 Broker[PUB]: Перебрасываем запрос в PUB сокет
134
+ App1 Broker[PUB]->A[SUB]: Отправитель получает ответ
135
+ </pre></div><script type="text/javascript" src="http://www.websequencediagrams.com/service.js"></script>
136
+
137
+ Балансировка
138
+ ============
139
+
140
+ Так как это ZMQ, мы можем очень просто изменить тип получения сообщения. Например, добавив балансер. Теперь можно запускать процессы со ссылкой на этот балансер.
141
+
142
+ Zactor.start :balancer => "0.0.0.0:4000"
64
143
 
65
- При наследовании класса с интерфейсом, создается новый класс интерфейса и наследуется от предыдущего, т.е. повторяет иерархию класса, в который он включен.
144
+ У нас получится примерно следующая схема:
66
145
 
67
- Если в блоке <tt>interface</tt> вызывается метод <tt>interfaced</tt>, то исполнение блока, передаваемого <tt>interfaced</tt>
68
- происходит после добавления интерфейса в класс, в контексте этого класса.
146
+ ![ZMQ](images/zmq2.png)
69
147
 
70
- Пример:
71
-
72
- module A
73
- extend RubyInterface
74
- interface :int do
75
- interfaced do
76
- def baz
77
- self.class.int_interface.foo
78
- end
79
- end
80
-
81
- def self.foo
82
- "bar"
83
- end
84
- end
85
- end
86
-
87
- class B
88
- include A
89
- end
90
-
91
- B.new.baz # => "bar"
92
-
93
- В каждом модуле может быть определено произвольное количество интерфейсов
148
+ Теперь наш ping можно отправлять в балансер, а отвечать будет один из подключенных воркеров.
149
+
150
+ ping Zactor.get_actor("b", :host => "0.0.0.0:4000")
151
+
152
+
153
+ Протокол обмена
154
+ ===============
155
+
156
+
157
+ Perfomance
158
+ ==========
159
+
160
+ А хрен его знает, толком не мерялось ничего :)
161
+
162
+ TODO
163
+ ====
164
+
165
+ * Сделать событие отваливания объектов. Наверное, что-то вроде простого аналога link в эрланге.
166
+ * Добавить таймауты для запросов с коллбэками. Сейчас они будут висеть бесконечно и засрут память.
167
+ * Доступ к отправителю в колллбэке запроса. В случае с балансировкой он будет не тем же, кому мы посылали сообщение
data/Rakefile CHANGED
@@ -1,5 +1,3 @@
1
- # -*- coding: utf-8 -*-
2
-
3
1
  require 'rubygems'
4
2
  require 'bundler'
5
3
  begin
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.3
@@ -0,0 +1,88 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ # ruby client.rb SELF_PORT SERVER LOGIN
4
+ # ruby client.rb 6001 0.0.0.0:8000 lolo
5
+ require 'bundler'
6
+ ENV['BUNDLE_GEMFILE'] = File.join(File.dirname(__FILE__), '..', '..', 'Gemfile')
7
+
8
+ Bundler.setup(:default)
9
+
10
+ require 'zactor'
11
+ class Client
12
+ include Zactor
13
+
14
+ zactor do
15
+ event(:message) do |o, msg, text|
16
+ puts text
17
+ end
18
+ end
19
+
20
+ def initialize(login)
21
+ @login = login
22
+ @persons = {}
23
+ end
24
+
25
+ def start(server)
26
+ zactor.init
27
+ @server = Zactor.get_actor("server", :host => server)
28
+ connect
29
+ end
30
+
31
+ def connect
32
+ puts "Подключаемся"
33
+ zactor.send_request(@server, :new_client, @login) do
34
+ puts "Поключились!"
35
+ zactor.link(@server) { connect }
36
+ end.timeout(5) { puts "Проблемы с подключением..." }
37
+ end
38
+
39
+ def send_message(text)
40
+ if text =~ /(\w+) -> (.+)/
41
+ send_personal($1, $2)
42
+ else
43
+ zactor.send_request(@server, :message, text)
44
+ end
45
+ end
46
+
47
+ def send_personal(login, text)
48
+ if client = @persons[login]
49
+ zactor.send_request(client, :message, "(personally) #{@login}:" + "#{text}")
50
+ else
51
+ zactor.send_request(@server, :client_request, login) do |res, client|
52
+ case res
53
+ when :ok
54
+ @persons[login] = client
55
+ zactor.link(client) { @persons.delete login }
56
+ zactor.send_request(client, :message, "(personally) #{@login}:" + "#{text}")
57
+ else
58
+ puts "Ошибка отправки сообщения"
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ def stop
65
+ zactor.finish
66
+ EM.stop
67
+ end
68
+
69
+ module KeyboardInput
70
+ include EM::Protocols::LineText2
71
+ attr_accessor :client
72
+ def receive_line(data)
73
+ client.send_message data
74
+ end
75
+ end
76
+
77
+ end
78
+
79
+ client = Client.new ARGV[2]
80
+
81
+ Signal.trap('INT') { client.stop }
82
+ Signal.trap('TERM') { client.stop }
83
+
84
+ EM.run do
85
+ Zactor.start ARGV[0]#, :debug => true
86
+ client.start ARGV[1]
87
+ EM.open_keyboard(Client::KeyboardInput) { |c| c.client = client }
88
+ end