thespian 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.8.7
4
+ - 1.9.2
5
+ - 1.9.3
6
+ - jruby-19mode
7
+ - jruby-18mode
data/CHANGELOG CHANGED
@@ -1,2 +1,10 @@
1
+ 0.1.0
2
+ * Added support for different modes
3
+ * Threaded mode (default)
4
+ * Fibered mode
5
+ * Added Thespian.actor{ ... } as alias for Thespian.actor.receive{ ... }
6
+ * Added Thespian::Actor#mailbox_size
7
+
8
+
1
9
  0.0.1
2
10
  * Initial release
data/README.rdoc CHANGED
@@ -1,6 +1,6 @@
1
- == Thespian
1
+ == Thespian {<img src="https://secure.travis-ci.org/cjbottaro/thespian.png" />}[http://travis-ci.org/cjbottaro/thespian]
2
2
 
3
- Implementation of the actor pattern built on threads.
3
+ Implementation of the actor pattern built for use with threads and/or fibers.
4
4
 
5
5
  == Quickstart
6
6
 
@@ -66,7 +66,7 @@ an error occurred).
66
66
 
67
67
  What happens if an actor causes an unhandled exception while processing a message? The actor's
68
68
  state will be set to +:finished+, Thespian::Actor#error? will return true and subsequent calls to
69
- various methods will cause the exception to be raised in the calling thread.
69
+ various methods will cause the exception to be raised in the calling thread or fiber.
70
70
 
71
71
  actor = Thespian::Actor.new do |message|
72
72
  raise "oops"
@@ -162,6 +162,49 @@ to it's parent object.
162
162
  What does that mean? Nothing really. The only difference is that Thespian::DeadActorError#actor
163
163
  will return an instance of ArnoldSchwarzenegger instead of an instance of Thespian::Actor.
164
164
 
165
+ == Fibers
166
+
167
+ Actors run on threads by default, but you can seamlessly run them on fibers instead:
168
+
169
+ actor = Thespian::Actor.new(:mode => :fiber){ ... }
170
+
171
+ Or:
172
+
173
+ class ArnoldSchwarzenegger
174
+ include Thespian
175
+
176
+ # Any of these will work
177
+
178
+ actor(:mode => :fiber).receive{ |message| ... }
179
+
180
+ actor.options[:mode] = :fiber
181
+ end
182
+
183
+ When running in fibered mode, be sure that your actors are executing inside EventMachine's reactor loop.
184
+ Also be sure that there is a root fiber. In other words, something like...
185
+
186
+ EventMachine.run do
187
+ Fiber.new do
188
+
189
+ ###
190
+ # Your actor code here
191
+ ###
192
+
193
+ EventMachine.stop
194
+ end.resume
195
+ end
196
+
197
+ Thespian uses {Strand}[https://github.com/cjbottaro/strand] behind the scenes which to deal with fibers.
198
+ {Strand}[https://github.com/cjbottaro/strand] is an abstraction that makes fibers behave like threads when
199
+ used with EventMachine. You will need to install {Strand}[https://github.com/cjbottaro/strand] to use
200
+ Thespian in fibered mode:
201
+
202
+ gem "strand"
203
+
204
+ You can change the default mode for all Actors with the following:
205
+
206
+ Thespian::Actor::DEFAULT_OPTIONS[:mode] = :fiber
207
+
165
208
  == RDoc
166
209
 
167
210
  {\http://doc.stochasticbytes.com/thespian/index.html}[http://doc.stochasticbytes.com/thespian/index.html]
@@ -169,10 +212,13 @@ will return an instance of ArnoldSchwarzenegger instead of an instance of Thespi
169
212
  == Examples
170
213
 
171
214
  A simple producer/consumer example.
215
+
172
216
  {\https://github.com/cjbottaro/thespian/blob/master/examples/producer_consumer.rb}[https://github.com/cjbottaro/thespian/blob/master/examples/producer_consumer.rb]
173
217
 
174
218
  An example where one actor links to another.
219
+
175
220
  {\https://github.com/cjbottaro/thespian/blob/master/examples/linked.rb}[https://github.com/cjbottaro/thespian/blob/master/examples/linked.rb]
176
221
 
177
222
  A complex example showing how a background job/task processor could be architected.
223
+
178
224
  {\https://github.com/cjbottaro/thespian/blob/master/examples/task_processor.rb}[https://github.com/cjbottaro/thespian/blob/master/examples/task_processor.rb]
data/Rakefile CHANGED
@@ -1 +1,9 @@
1
1
  require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ task :default => :spec
5
+
6
+ desc "Run specs (default)"
7
+ RSpec::Core::RakeTask.new do |t|
8
+ # Put spec opts in a file named .rspec in root
9
+ end
data/examples/linked.rb CHANGED
@@ -1,39 +1,45 @@
1
1
  require "thespian"
2
+ require "thespian/example"
2
3
 
3
- producer = nil
4
- consumer = nil
5
- n = 0
4
+ Thespian::Example.run do
6
5
 
7
- producer = Thespian::Actor.new do |message|
8
- case message
9
- when :need_item
10
- if (n += 1) < 10
11
- consumer << rand
6
+ producer = nil
7
+ consumer = nil
8
+ n = 0
9
+
10
+ producer = Thespian::Actor.new do |message|
11
+ case message
12
+ when :need_item
13
+ if (n += 1) < 10
14
+ consumer << rand
15
+ else
16
+ consumer << "bad message"
17
+ end
12
18
  else
13
- consumer << "bad message"
19
+ raise "unexpected message: #{message}"
14
20
  end
15
- else
16
- raise "unexpected message: #{message}"
17
21
  end
18
- end
19
22
 
20
- consumer = Thespian::Actor.new do |message|
21
- case message
22
- when Float
23
- puts "consumer got #{message}"
24
- producer << :need_item
25
- else
26
- raise "I can only work with numbers!"
23
+ consumer = Thespian::Actor.new do |message|
24
+ case message
25
+ when Float
26
+ puts "consumer got #{message}"
27
+ producer << :need_item
28
+ else
29
+ raise "I can only work with numbers!"
30
+ end
27
31
  end
28
- end
29
32
 
30
- producer.link(consumer)
31
- producer.start
32
- consumer.start
33
- producer << :need_item
34
- Thread.pass while consumer.running?
35
- puts consumer.finished? # True
36
- puts consumer.error? # True
37
- puts producer.finished? # True because it's linked to the consumer
38
- puts producer.error? # True because it's linked to the consumer
39
- puts producer.exception
33
+ producer.link(consumer)
34
+ producer.start
35
+ consumer.start
36
+ producer << :need_item
37
+ pass while consumer.running?
38
+ pass while producer.running?
39
+ puts consumer.finished? # True
40
+ puts consumer.error? # True
41
+ puts producer.finished? # True because it's linked to the consumer
42
+ puts producer.error? # True because it's linked to the consumer
43
+ puts producer.exception
44
+
45
+ end
@@ -1,24 +1,32 @@
1
1
  require "thespian"
2
+ require "thespian/example"
2
3
 
3
- producer = nil
4
- consumer = nil
4
+ Thespian::Example.run do
5
5
 
6
- producer = Thespian::Actor.new do |message|
7
- case message
8
- when :need_item
9
- consumer << rand
10
- else
11
- raise "unexpected message: #{message}"
6
+ producer = nil
7
+ consumer = nil
8
+
9
+ producer = Thespian::Actor.new do |message|
10
+ case message
11
+ when :need_item
12
+ consumer << rand
13
+ else
14
+ raise "unexpected message: #{message}"
15
+ end
16
+ end
17
+
18
+ consumer = Thespian::Actor.new do |message|
19
+ sleep(message)
20
+ puts "consumer got #{message}"
21
+ producer << :need_item
12
22
  end
13
- end
14
23
 
15
- consumer = Thespian::Actor.new do |message|
16
- sleep(message)
17
- puts "consumer got #{message}"
24
+ producer.start
25
+ consumer.start
18
26
  producer << :need_item
19
- end
27
+ while true
28
+ sleep(1)
29
+ puts "Total threads: #{Thread.list.length}"
30
+ end
20
31
 
21
- producer.start
22
- consumer.start
23
- producer << :need_item
24
- sleep
32
+ end
@@ -1,4 +1,5 @@
1
1
  require "thespian"
2
+ require "thespian/example"
2
3
 
3
4
  # The actors are Supervisor, Logger, Poller, Worker(s). There is one of each except for multiple
4
5
  # workers. The basic idea is that the supervisor can send messages to any other actor, but all
@@ -114,7 +115,7 @@ class Supervisor
114
115
  do_log("worker(#{actor.id}) died, restarting")
115
116
  initialize_worker(actor.id)
116
117
  when Poller
117
- do_log("poller died (#{@ready.length}, #{@poller.actor.instance_eval{ @mailbox }.length}), restarting")
118
+ do_log("poller died (#{@ready.length}, #{@poller.actor.mailbox_size}), restarting")
118
119
  initialize_poller
119
120
  end
120
121
  end
@@ -163,8 +164,11 @@ class Supervisor
163
164
 
164
165
  end
165
166
 
166
- s = Supervisor.new(5)
167
- while true
168
- raise s.actor.exception unless s.actor.running?
169
- sleep(1)
167
+ Thespian::Example.run do
168
+ s = Supervisor.new(5)
169
+ while true
170
+ sleep(1)
171
+ raise s.actor.exception unless s.actor.running?
172
+ puts "Total threads: #{Thread.list.length}"
173
+ end
170
174
  end
data/lib/thespian.rb CHANGED
@@ -2,6 +2,8 @@ require "thespian/actor"
2
2
  require "thespian/dsl"
3
3
  require "thespian/version"
4
4
 
5
+ require "thespian/strategies/interface"
6
+
5
7
  # Include this module into classes to give them acting abilities.
6
8
  # class ArnoldSchwarzenegger
7
9
  # include Thespian
@@ -30,8 +32,14 @@ module Thespian
30
32
 
31
33
  module ClassMethods #:nodoc:
32
34
 
33
- def actor
35
+ def actor(options = nil, &block)
34
36
  @actor ||= Dsl.new
37
+ @actor.options = options if options
38
+ if block_given?
39
+ @actor.receive(&block)
40
+ else
41
+ @actor
42
+ end
35
43
  end
36
44
 
37
45
  end
@@ -39,7 +47,7 @@ module Thespian
39
47
  module InstanceMethods #:nodoc:
40
48
 
41
49
  def actor
42
- @actor ||= Actor.new(:object => self, &self.class.actor.receive_block)
50
+ @actor ||= Actor.new(self.class.actor.options.merge(:object => self), &self.class.actor.receive_block)
43
51
  end
44
52
 
45
53
  end
@@ -13,6 +13,9 @@ module Thespian
13
13
  # Returns the actor's state.
14
14
  attr_reader :state
15
15
 
16
+ attr_reader :strategy
17
+ private :strategy
18
+
16
19
  # The state an actor is in after it has been created, but before it enters the message processing loop.
17
20
  STATE_INITIALIZED = :initialized
18
21
 
@@ -23,6 +26,7 @@ module Thespian
23
26
  STATE_FINISHED = :finished
24
27
 
25
28
  DEFAULT_OPTIONS = {
29
+ :mode => :thread,
26
30
  :strict => true,
27
31
  :trap_exit => false,
28
32
  :object => nil
@@ -42,10 +46,12 @@ module Thespian
42
46
  @receive_block = block
43
47
  @linked_actors = Set.new
44
48
 
45
- @mailbox = []
46
- @mailbox_lock = Monitor.new
47
- @mailbox_cond = @mailbox_lock.new_cond
49
+ @strategy = strategy_class.new{ run }
50
+ end
48
51
 
52
+ def strategy_class #:nodoc:
53
+ class_name = options[:mode].to_s.capitalize
54
+ Strategy.const_get(class_name)
49
55
  end
50
56
 
51
57
  # call-seq:
@@ -54,6 +60,9 @@ module Thespian
54
60
  # options(symbol) -> value
55
61
  #
56
62
  # Get or set options. Valid options are:
63
+ # [:mode (default :thread)]
64
+ # What mode to create the actor in. Choices are +:fiber+ or +:thread+. When running in fibered mode,
65
+ # be sure that EventMachine's reactor is running and there is a root fiber.
57
66
  # [:strict (default true)]
58
67
  # Require an actor to be running in order to put messages into its mailbox.
59
68
  # [:trap_exit (default false)]
@@ -106,25 +115,7 @@ module Thespian
106
115
  # before staring the thread.
107
116
  @state = :running
108
117
 
109
- # Declare local synchronization vars.
110
- lock = Monitor.new
111
- cond = lock.new_cond
112
- wait = true
113
-
114
- # Start the thread and have it signal when it's running.
115
- @thread = Thread.new do
116
- Thread.current[:actor] = options(:object).to_s
117
- lock.synchronize do
118
- wait = false
119
- cond.signal
120
- end
121
- run
122
- end
123
-
124
- # Block until the thread has signaled that it's running.
125
- lock.synchronize do
126
- cond.wait_while{ wait }
127
- end
118
+ @strategy.start
128
119
  end
129
120
 
130
121
  # Add a message to the actor's mailbox.
@@ -132,10 +123,7 @@ module Thespian
132
123
  def <<(message)
133
124
  check_alive! if options(:strict)
134
125
  message = message.new if message == Stop
135
- @mailbox_lock.synchronize do
136
- @mailbox << message
137
- @mailbox_cond.signal
138
- end
126
+ @strategy << message
139
127
  self
140
128
  end
141
129
 
@@ -145,7 +133,7 @@ module Thespian
145
133
  def stop
146
134
  check_alive! if options(:strict)
147
135
  self << Stop.new
148
- @thread.join
136
+ @strategy.stop
149
137
  raise @exception if @exception
150
138
  end
151
139
 
@@ -169,12 +157,17 @@ module Thespian
169
157
  !!@exception
170
158
  end
171
159
 
160
+ # Returns how many messages are in the actor's mailbox.
161
+ def mailbox_size
162
+ @strategy.mailbox_size
163
+ end
164
+
172
165
  # Salvage mailbox contents from a dead actor (including the message it died on).
173
166
  # Useful for restarting a dead actor while preserving its mailbox.
174
167
  def salvage_mailbox
175
- raise "cannot salvage mailbox from an actor that isn't finished" unless state == :finished
176
- @mailbox.dup.tap do |mailbox|
177
- mailbox.unshift(@last_message) if @last_message
168
+ raise "cannot salvage mailbox from an actor that isn't finished" unless finished?
169
+ @strategy.messages.tap do |messages|
170
+ messages.unshift(@last_message) if @last_message
178
171
  end
179
172
  end
180
173
 
@@ -202,13 +195,9 @@ module Thespian
202
195
 
203
196
  # Receive a message from the actor's mailbox.
204
197
  def receive
205
- message = @mailbox_lock.synchronize do
206
- @mailbox_cond.wait_while{ @mailbox.empty? }
207
- @message = @mailbox.shift
208
- end
209
198
 
210
199
  # Communicate with #run by possibly raising exceptions here.
211
- case message
200
+ case (message = @strategy.receive)
212
201
  when DeadActorError
213
202
  raise message unless options(:trap_exit)
214
203
  when Stop
data/lib/thespian/dsl.rb CHANGED
@@ -1,6 +1,11 @@
1
1
  module Thespian
2
2
  class Dsl #:nodoc:
3
3
  attr_reader :receive_block
4
+ attr_accessor :options
5
+
6
+ def initialize
7
+ @options = {}
8
+ end
4
9
 
5
10
  def receive(&block)
6
11
  @receive_block = block
@@ -0,0 +1,41 @@
1
+ module Thespian
2
+ # This class aids in running the examples in the different modes.
3
+ #
4
+ # To run in threaded mode, just run without arguments:
5
+ # bundle exec ruby examples/task_processor.rb
6
+ # To run in fibered mode:
7
+ # bundle exec ruby examples/task_processor.rb --fiber
8
+ # This class does all the necessary incantations necessary to run
9
+ # in fibered mode (starts/stops EventMachine, wraps in root fiber, etc).
10
+ class Example
11
+
12
+ # Determine what mode to run in by looking at ARGV then invoke the block given.
13
+ # If running in fibered mode, the block will be wrapped with necessary calls
14
+ # to EventMachine and Fiber. It will also make sure to define fiber
15
+ # safe versions of #sleep and #pass that the example can use.
16
+ def self.run(&example)
17
+ if ARGV.include?("--fiber") || ARGV.include?("--fibers")
18
+ require "eventmachine"
19
+ require "strand"
20
+ EM.run do
21
+ Strand.new do
22
+ puts "Running example with fibers..."
23
+ Thespian::Actor::DEFAULT_OPTIONS[:mode] = :fiber
24
+ example.binding.eval <<-CODE
25
+ def sleep(n); Strand.sleep(n); end
26
+ def pass; Strand.pass; end
27
+ CODE
28
+ example.call
29
+ EM.stop
30
+ end
31
+ end
32
+ else
33
+ puts "Running example with threads..."
34
+ example.binding.eval <<-CODE
35
+ def pass; Thread.pass; end
36
+ CODE
37
+ example.call
38
+ end
39
+ end
40
+ end
41
+ end