tribe 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile.lock +3 -3
- data/README.md +149 -16
- data/lib/tribe/actable.rb +87 -39
- data/lib/tribe/exceptions.rb +3 -0
- data/lib/tribe/future.rb +98 -0
- data/lib/tribe/version.rb +1 -1
- data/lib/tribe.rb +2 -0
- data/tribe.gemspec +1 -1
- metadata +6 -4
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -30,49 +30,44 @@ Or install it yourself as:
|
|
30
30
|
def initialize(options = {})
|
31
31
|
super
|
32
32
|
end
|
33
|
-
|
33
|
+
|
34
34
|
def on_my_custom(event)
|
35
35
|
puts "Received a custom event (#{event.inspect})"
|
36
36
|
end
|
37
|
-
|
37
|
+
|
38
38
|
def exception_handler(e)
|
39
39
|
super
|
40
40
|
puts concat_e("MyActor (#{identifier}) died.", e)
|
41
41
|
end
|
42
|
-
|
42
|
+
|
43
43
|
def shutdown_handler(event)
|
44
44
|
super
|
45
45
|
puts "MyActor (#{identifier}) is shutting down. Put cleanup code here."
|
46
46
|
end
|
47
47
|
end
|
48
|
-
|
48
|
+
|
49
49
|
# Create some named actors.
|
50
50
|
100.times do |i|
|
51
51
|
MyActor.new(:name => "my_actor_#{i}")
|
52
52
|
end
|
53
|
-
|
53
|
+
|
54
54
|
# Send an event to each actors. Find each actor using the global registry.
|
55
55
|
100.times do |i|
|
56
56
|
actor = Tribe.registry["my_actor_#{i}"]
|
57
57
|
actor.enqueue(:my_custom, 'hello world')
|
58
58
|
end
|
59
|
-
|
59
|
+
|
60
60
|
# Shutdown the actors.
|
61
61
|
100.times do |i|
|
62
62
|
actor = Tribe.registry["my_actor_#{i}"]
|
63
63
|
actor.enqueue(:shutdown)
|
64
64
|
end
|
65
65
|
|
66
|
-
|
67
|
-
Because actors use a shared thread pool, it is important that they don't block for long periods of time (short periods are fine).
|
66
|
+
#### Implementation notes
|
67
|
+
*Important*: Because actors use a shared thread pool, it is important that they don't block for long periods of time (short periods are fine).
|
68
68
|
Actors that block for long periods of time should use a dedicated thread (:dedicated => true or subclass from Tribe::DedicatedActor).
|
69
69
|
|
70
|
-
|
71
|
-
|
72
|
-
Registries hold references to named actors.
|
73
|
-
In general you shouldn't have to create your own since there is a global one (Tribe.registry).
|
74
|
-
|
75
|
-
## Options (defaults below):
|
70
|
+
#### Options (defaults below):
|
76
71
|
|
77
72
|
actor = Tribe::Actor.new(
|
78
73
|
:logger => nil, # Ruby logger instance.
|
@@ -90,6 +85,17 @@ In general you shouldn't have to create your own since there is a global one (Tr
|
|
90
85
|
:name => nil # The name of the actor (must be unique in the registry).
|
91
86
|
)
|
92
87
|
|
88
|
+
## Registries
|
89
|
+
|
90
|
+
Registries hold references to named actors so that you can easily find them.
|
91
|
+
In general you shouldn't have to create your own since there is a global one (Tribe.registry).
|
92
|
+
|
93
|
+
actor = Tribe::Actor.new(:name => 'some_actor')
|
94
|
+
|
95
|
+
if actor == Tribe.registry['some_actor']
|
96
|
+
puts 'Successfully found some_actor in the registry.'
|
97
|
+
end
|
98
|
+
|
93
99
|
## Timers
|
94
100
|
|
95
101
|
Actors can create timers to perform some work in the future.
|
@@ -127,10 +133,137 @@ Both one-shot and periodic timers are provides.
|
|
127
133
|
actor.enqueue(:shutdown)
|
128
134
|
end
|
129
135
|
|
136
|
+
## Futures (experimental)
|
137
|
+
|
138
|
+
Futures allow an actor to ask another actor to perform a computation and then return the result.
|
139
|
+
Tribe includes both blocking and non-blocking actors.
|
140
|
+
You should prefer to use non-blocking actors in your code when possible due to performance reasons (see details below).
|
141
|
+
|
142
|
+
#### Non-blocking
|
143
|
+
|
144
|
+
Non-blocking actors are asynchronous and use callbacks.
|
145
|
+
No waiting for a result is involved.
|
146
|
+
The actor will continue to process other events.
|
147
|
+
|
148
|
+
class ActorA < Tribe::Actor
|
149
|
+
private
|
150
|
+
def exception_handler(e)
|
151
|
+
super
|
152
|
+
puts concat_e("ActorA (#{identifier}) died.", e)
|
153
|
+
end
|
154
|
+
|
155
|
+
def on_start(event)
|
156
|
+
friend = registry['actor_b']
|
157
|
+
|
158
|
+
future = friend.enqueue_future(:compute, 10)
|
159
|
+
|
160
|
+
future.success do |result|
|
161
|
+
perform do
|
162
|
+
puts "ActorA (#{identifier}) future result: #{result}"
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
future.failure do |exception|
|
167
|
+
perform do
|
168
|
+
puts "ActorA (#{identifier}) future failure: #{exception}"
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
class ActorB < Tribe::Actor
|
175
|
+
def exception_handler(e)
|
176
|
+
super
|
177
|
+
puts concat_e("ActorB (#{identifier}) died.", e)
|
178
|
+
end
|
179
|
+
|
180
|
+
def on_compute(event)
|
181
|
+
return factorial(event.data)
|
182
|
+
end
|
183
|
+
|
184
|
+
def factorial(num)
|
185
|
+
return 1 if num <= 0
|
186
|
+
return num * factorial(num - 1)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
actor_a = ActorA.new(:name => 'actor_a')
|
191
|
+
actor_b = ActorB.new(:name => 'actor_b')
|
192
|
+
|
193
|
+
actor_a.enqueue(:start)
|
194
|
+
|
195
|
+
actor_a.enqueue(:shutdown)
|
196
|
+
actor_b.enqueue(:shutdown)
|
197
|
+
|
198
|
+
*Important*: You must use Actor#perform inside the above callbacks.
|
199
|
+
This ensures that your code executes within the context of the correct actor.
|
200
|
+
Failure to do so will result in race conditions and other nasty things.
|
201
|
+
|
202
|
+
#### Blocking
|
203
|
+
|
204
|
+
Blocking actors are synchronous.
|
205
|
+
The actor won't process any other events until the future has a result.
|
206
|
+
|
207
|
+
class ActorA < Tribe::Actor
|
208
|
+
private
|
209
|
+
def exception_handler(e)
|
210
|
+
super
|
211
|
+
puts concat_e("ActorA (#{identifier}) died.", e)
|
212
|
+
end
|
213
|
+
|
214
|
+
def on_start(event)
|
215
|
+
friend = registry['actor_b']
|
216
|
+
|
217
|
+
future = friend.enqueue_future(:compute, 10)
|
218
|
+
|
219
|
+
future.wait # The current thread will sleep until a result is available.
|
220
|
+
|
221
|
+
if future.success?
|
222
|
+
puts "ActorA (#{identifier}) future result: #{future.result}"
|
223
|
+
else
|
224
|
+
puts "ActorA (#{identifier}) future failure: #{future.result}"
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
class ActorB < Tribe::Actor
|
230
|
+
def exception_handler(e)
|
231
|
+
super
|
232
|
+
puts concat_e("ActorB (#{identifier}) died.", e)
|
233
|
+
end
|
234
|
+
|
235
|
+
def on_compute(event)
|
236
|
+
return factorial(event.data)
|
237
|
+
end
|
238
|
+
|
239
|
+
def factorial(num)
|
240
|
+
return 1 if num <= 0
|
241
|
+
return num * factorial(num - 1)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
actor_a = ActorA.new(:name => 'actor_a')
|
246
|
+
actor_b = ActorB.new(:name => 'actor_b')
|
247
|
+
|
248
|
+
actor_a.enqueue(:start)
|
249
|
+
|
250
|
+
actor_a.enqueue(:shutdown)
|
251
|
+
actor_b.enqueue(:shutdown)
|
252
|
+
|
253
|
+
#### Futures and Performance
|
254
|
+
|
255
|
+
You should prefer non-blocking futures as much as possible in your application code.
|
256
|
+
This is because blocking futures (Future#wait) causes the current actor (and thread) to sleep.
|
257
|
+
|
258
|
+
Tribe is designed specifically to support having a large number of actors running on a small number of threads.
|
259
|
+
Thus, you will run into performance and/or deadlock problems if too many actors are waiting at the same time.
|
260
|
+
|
261
|
+
If you choose to use blocing futures then it is highly recommended that you only use them with dedicated actors.
|
262
|
+
Each dedicated actor runs in a separate thread (instead of a shared thread pool).
|
263
|
+
The downside to using dedicated actors is that they consume more resources and you can't have as many of them.
|
264
|
+
|
130
265
|
## TODO - missing features
|
131
266
|
|
132
|
-
- Futures.
|
133
|
-
- Workers::Timer integration.
|
134
267
|
- Supervisors.
|
135
268
|
- Linking.
|
136
269
|
|
data/lib/tribe/actable.rb
CHANGED
@@ -1,80 +1,132 @@
|
|
1
|
+
# This module is designed to be mixed in with your application code.
|
2
|
+
# Because of this, all instance variables are prefixed with an underscore.
|
3
|
+
# The hope is to minimize the chances of conflicts.
|
4
|
+
# Long term my goal is to move all of these variables into an ActorState object.
|
5
|
+
|
1
6
|
module Tribe
|
2
7
|
module Actable
|
3
8
|
include Workers::Helpers
|
4
9
|
|
5
|
-
|
6
|
-
@logger = Workers::LogProxy.new(options[:logger])
|
7
|
-
@dedicated = options[:dedicated] || false
|
8
|
-
@mailbox = options[:mailbox] || Tribe::Mailbox.new
|
9
|
-
@registry = options[:registry] || Tribe.registry
|
10
|
-
@scheduler = options[:scheduler] || Workers.scheduler
|
11
|
-
@timers = Tribe::SafeSet.new
|
12
|
-
@name = options[:name]
|
13
|
-
@pool = @dedicated ? Workers::Pool.new(:size => 1) : (options[:pool] || Workers.pool)
|
14
|
-
@alive = true
|
10
|
+
private
|
15
11
|
|
16
|
-
|
12
|
+
def init_actable(options = {})
|
13
|
+
@_logger = Workers::LogProxy.new(options[:logger])
|
14
|
+
@_dedicated = options[:dedicated] || false
|
15
|
+
@_mailbox = options[:mailbox] || Tribe::Mailbox.new
|
16
|
+
@_registry = options[:registry] || Tribe.registry
|
17
|
+
@_scheduler = options[:scheduler] || Workers.scheduler
|
18
|
+
@_timers = Tribe::SafeSet.new
|
19
|
+
@_name = options[:name]
|
20
|
+
@_pool = @_dedicated ? Workers::Pool.new(:size => 1) : (options[:pool] || Workers.pool)
|
21
|
+
@_alive = true
|
22
|
+
@_futures = Tribe::SafeSet.new
|
23
|
+
|
24
|
+
@_registry.register(self)
|
17
25
|
end
|
18
26
|
|
27
|
+
public
|
28
|
+
|
19
29
|
def enqueue(command, data = nil)
|
20
30
|
return false unless alive?
|
21
31
|
|
22
|
-
@
|
23
|
-
@
|
32
|
+
@_mailbox.push(Workers::Event.new(command, data)) do
|
33
|
+
@_pool.perform { process_events }
|
24
34
|
end
|
25
35
|
|
26
36
|
return true
|
27
37
|
end
|
28
38
|
|
39
|
+
def enqueue_future(command, data = nil)
|
40
|
+
future = Tribe::Future.new
|
41
|
+
@_futures.add(future)
|
42
|
+
|
43
|
+
perform do
|
44
|
+
begin
|
45
|
+
result = result = process_event(Workers::Event.new(command, data))
|
46
|
+
future.result = result
|
47
|
+
rescue Exception => e
|
48
|
+
future.result = e
|
49
|
+
raise
|
50
|
+
ensure
|
51
|
+
@_futures.delete(future)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
return future
|
56
|
+
end
|
57
|
+
|
29
58
|
def alive?
|
30
|
-
@
|
59
|
+
@_mailbox.synchronize { return @_alive }
|
31
60
|
end
|
32
61
|
|
33
62
|
def name
|
34
|
-
return @
|
63
|
+
return @_name
|
35
64
|
end
|
36
65
|
|
37
66
|
def identifier
|
38
|
-
return @
|
67
|
+
return @_name ? "#{object_id}:#{@_name}" : object_id
|
68
|
+
end
|
69
|
+
|
70
|
+
def shutdown
|
71
|
+
return enqueue(:shutdown)
|
72
|
+
end
|
73
|
+
|
74
|
+
def perform(&block)
|
75
|
+
return enqueue(:perform, block)
|
39
76
|
end
|
40
77
|
|
41
78
|
private
|
42
79
|
|
80
|
+
def registry
|
81
|
+
return @_registry
|
82
|
+
end
|
83
|
+
|
84
|
+
def pool
|
85
|
+
return @_pool
|
86
|
+
end
|
87
|
+
|
88
|
+
def logger
|
89
|
+
return @_logger
|
90
|
+
end
|
91
|
+
|
43
92
|
def process_events
|
44
|
-
while (event = @
|
93
|
+
while (event = @_mailbox.shift)
|
45
94
|
case event.command
|
46
95
|
when :shutdown
|
47
96
|
cleanup
|
48
97
|
shutdown_handler(event)
|
98
|
+
when :perform
|
99
|
+
perform_handler(event)
|
49
100
|
else
|
50
101
|
process_event(event)
|
51
102
|
end
|
52
103
|
end
|
53
104
|
|
54
105
|
rescue Exception => e
|
55
|
-
cleanup
|
106
|
+
cleanup(e)
|
56
107
|
exception_handler(e)
|
57
108
|
ensure
|
58
|
-
@
|
59
|
-
@
|
109
|
+
@_mailbox.release do
|
110
|
+
@_pool.perform { process_events if @_alive }
|
60
111
|
end
|
61
112
|
|
62
113
|
return nil
|
63
114
|
end
|
64
115
|
|
65
|
-
def cleanup
|
66
|
-
@
|
67
|
-
@
|
68
|
-
@
|
116
|
+
def cleanup(e = nil)
|
117
|
+
@_pool.shutdown if @_dedicated
|
118
|
+
@_mailbox.synchronize { @_alive = false }
|
119
|
+
@_registry.unregister(self)
|
120
|
+
@_timers.each { |t| t.cancel }
|
121
|
+
@_futures.each { |f| f.result = e || Tribe::ActorShutdownError.new }
|
69
122
|
|
70
123
|
return nil
|
71
124
|
end
|
72
125
|
|
73
126
|
# Override and call super as necessary.
|
127
|
+
# Note that the return value is used as the result of a future.
|
74
128
|
def process_event(event)
|
75
|
-
send("on_#{event.command}", event)
|
76
|
-
|
77
|
-
return nil
|
129
|
+
return send("on_#{event.command}", event)
|
78
130
|
end
|
79
131
|
|
80
132
|
# Override and call super as necessary.
|
@@ -84,40 +136,36 @@ module Tribe
|
|
84
136
|
|
85
137
|
# Override and call super as necessary.
|
86
138
|
def shutdown_handler(event)
|
87
|
-
shutdown_timers
|
88
|
-
|
89
139
|
return nil
|
90
140
|
end
|
91
141
|
|
92
|
-
def
|
93
|
-
|
94
|
-
timer.cancel
|
95
|
-
end
|
142
|
+
def perform_handler(event)
|
143
|
+
event.data.call
|
96
144
|
|
97
145
|
return nil
|
98
146
|
end
|
99
147
|
|
100
148
|
def timer(delay, command, data = nil)
|
101
|
-
timer = Workers::Timer.new(delay, :scheduler => @
|
102
|
-
@
|
149
|
+
timer = Workers::Timer.new(delay, :scheduler => @_scheduler) do
|
150
|
+
@_timers.delete(timer)
|
103
151
|
enqueue(command, data)
|
104
152
|
end
|
105
153
|
|
106
|
-
@
|
154
|
+
@_timers.add(timer)
|
107
155
|
|
108
156
|
return timer
|
109
157
|
end
|
110
158
|
|
111
159
|
def periodic_timer(delay, command, data = nil)
|
112
|
-
timer = Workers::PeriodicTimer.new(delay, :scheduler => @
|
160
|
+
timer = Workers::PeriodicTimer.new(delay, :scheduler => @_scheduler) do
|
113
161
|
enqueue(command, data)
|
114
162
|
unless alive?
|
115
|
-
@
|
163
|
+
@_timers.delete(timer)
|
116
164
|
timer.cancel
|
117
165
|
end
|
118
166
|
end
|
119
167
|
|
120
|
-
@
|
168
|
+
@_timers.add(timer)
|
121
169
|
|
122
170
|
return timer
|
123
171
|
end
|
data/lib/tribe/future.rb
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
module Tribe
|
2
|
+
class Future
|
3
|
+
def initialize
|
4
|
+
@state = :initialized
|
5
|
+
@mutex = Mutex.new
|
6
|
+
@condition = ConditionVariable.new
|
7
|
+
@result = nil
|
8
|
+
@success_callback = nil
|
9
|
+
@failure_callback = nil
|
10
|
+
|
11
|
+
return nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def finished?
|
15
|
+
@mutex.synchronize do
|
16
|
+
return @state == :finished
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def result=(val)
|
21
|
+
@mutex.synchronize do
|
22
|
+
raise 'Result must only be set once.' unless @state == :initialized
|
23
|
+
|
24
|
+
@result = val
|
25
|
+
@state = :finished
|
26
|
+
@condition.signal
|
27
|
+
|
28
|
+
if val.is_a?(Exception)
|
29
|
+
@failure_callback.call(val) if @failure_callback
|
30
|
+
else
|
31
|
+
@success_callback.call(val) if @success_callback
|
32
|
+
end
|
33
|
+
|
34
|
+
return nil
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def result
|
39
|
+
@mutex.synchronize do
|
40
|
+
raise 'Result must be set first.' unless @state == :finished
|
41
|
+
|
42
|
+
return @result
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def wait
|
47
|
+
@mutex.synchronize do
|
48
|
+
return if @state == :finished
|
49
|
+
|
50
|
+
@condition.wait(@mutex)
|
51
|
+
|
52
|
+
return nil
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def success?
|
57
|
+
@mutex.synchronize do
|
58
|
+
raise 'Result must be set first.' unless @state == :finished
|
59
|
+
|
60
|
+
return !@result.is_a?(Exception)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def failure?
|
65
|
+
return !success?
|
66
|
+
end
|
67
|
+
|
68
|
+
def success(&block)
|
69
|
+
@mutex.synchronize do
|
70
|
+
case @state
|
71
|
+
when :initialized
|
72
|
+
@success_callback = block
|
73
|
+
when :finished
|
74
|
+
yield(@result) unless @result.is_a?(Exception)
|
75
|
+
else
|
76
|
+
raise 'Invalid state.'
|
77
|
+
end
|
78
|
+
|
79
|
+
return nil
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def failure(&block)
|
84
|
+
@mutex.synchronize do
|
85
|
+
case @state
|
86
|
+
when :initialized
|
87
|
+
@failure_callback = block
|
88
|
+
when :finished
|
89
|
+
yield(@result) if @result.is_a?(Exception)
|
90
|
+
else
|
91
|
+
raise 'Invalid state.'
|
92
|
+
end
|
93
|
+
|
94
|
+
return nil
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
data/lib/tribe/version.rb
CHANGED
data/lib/tribe.rb
CHANGED
@@ -3,11 +3,13 @@ require 'set'
|
|
3
3
|
require 'workers'
|
4
4
|
|
5
5
|
require 'tribe/safe_set'
|
6
|
+
require 'tribe/exceptions'
|
6
7
|
require 'tribe/mailbox'
|
7
8
|
require 'tribe/actable'
|
8
9
|
require 'tribe/actor'
|
9
10
|
require 'tribe/dedicated_actor'
|
10
11
|
require 'tribe/registry'
|
12
|
+
require 'tribe/future'
|
11
13
|
|
12
14
|
module Tribe
|
13
15
|
def self.registry
|
data/tribe.gemspec
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tribe
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-05-
|
12
|
+
date: 2013-05-25 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: workers
|
@@ -18,7 +18,7 @@ dependencies:
|
|
18
18
|
requirements:
|
19
19
|
- - '='
|
20
20
|
- !ruby/object:Gem::Version
|
21
|
-
version: 0.1.
|
21
|
+
version: 0.1.2
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
24
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -26,7 +26,7 @@ dependencies:
|
|
26
26
|
requirements:
|
27
27
|
- - '='
|
28
28
|
- !ruby/object:Gem::Version
|
29
|
-
version: 0.1.
|
29
|
+
version: 0.1.2
|
30
30
|
description: Tribe is a Ruby gem that implements event-driven actors.
|
31
31
|
email:
|
32
32
|
- chad@remesch.com
|
@@ -44,6 +44,8 @@ files:
|
|
44
44
|
- lib/tribe/actable.rb
|
45
45
|
- lib/tribe/actor.rb
|
46
46
|
- lib/tribe/dedicated_actor.rb
|
47
|
+
- lib/tribe/exceptions.rb
|
48
|
+
- lib/tribe/future.rb
|
47
49
|
- lib/tribe/mailbox.rb
|
48
50
|
- lib/tribe/registry.rb
|
49
51
|
- lib/tribe/safe_set.rb
|