tribe 0.1.0 → 0.2.0
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/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
|