zactor 0.0.5 → 0.0.6

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