thespian 0.0.1 → 0.1.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/.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