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 CHANGED
@@ -1,13 +1,13 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- tribe (0.0.11)
5
- workers (= 0.1.1)
4
+ tribe (0.1.0)
5
+ workers (= 0.1.2)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
- workers (0.1.1)
10
+ workers (0.1.2)
11
11
 
12
12
  PLATFORMS
13
13
  java
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
- ### Implementation notes
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
- ## Registries
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
- def init_actable(options = {})
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
- @registry.register(self)
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
- @mailbox.push(Workers::Event.new(command, data)) do
23
- @pool.perform { process_events }
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
- @mailbox.synchronize { return @alive }
59
+ @_mailbox.synchronize { return @_alive }
31
60
  end
32
61
 
33
62
  def name
34
- return @name
63
+ return @_name
35
64
  end
36
65
 
37
66
  def identifier
38
- return @name ? "#{object_id}:#{@name}" : object_id
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 = @mailbox.shift)
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
- @mailbox.release do
59
- @pool.perform { process_events if @alive }
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
- @pool.shutdown if @dedicated
67
- @mailbox.synchronize { @alive = false }
68
- @registry.unregister(self)
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 shutdown_timers
93
- @timers.each do |timer|
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 => @scheduler) do
102
- @timers.delete(timer)
149
+ timer = Workers::Timer.new(delay, :scheduler => @_scheduler) do
150
+ @_timers.delete(timer)
103
151
  enqueue(command, data)
104
152
  end
105
153
 
106
- @timers.add(timer)
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 => @scheduler) do
160
+ timer = Workers::PeriodicTimer.new(delay, :scheduler => @_scheduler) do
113
161
  enqueue(command, data)
114
162
  unless alive?
115
- @timers.delete(timer)
163
+ @_timers.delete(timer)
116
164
  timer.cancel
117
165
  end
118
166
  end
119
167
 
120
- @timers.add(timer)
168
+ @_timers.add(timer)
121
169
 
122
170
  return timer
123
171
  end
@@ -0,0 +1,3 @@
1
+ module Tribe
2
+ class ActorShutdownError < RuntimeError; end
3
+ end
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Tribe
2
- VERSION = '0.1.0'
2
+ VERSION = '0.2.0'
3
3
  end
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
@@ -15,5 +15,5 @@ Gem::Specification.new do |gem|
15
15
  gem.require_paths = ["lib"]
16
16
  gem.version = Tribe::VERSION
17
17
 
18
- gem.add_dependency('workers', '0.1.1')
18
+ gem.add_dependency('workers', '0.1.2')
19
19
  end
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.1.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-24 00:00:00.000000000 Z
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.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.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