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 +7 -0
- data/CHANGELOG +8 -0
- data/README.rdoc +49 -3
- data/Rakefile +8 -0
- data/examples/linked.rb +36 -30
- data/examples/producer_consumer.rb +25 -17
- data/examples/task_processor.rb +9 -5
- data/lib/thespian.rb +10 -2
- data/lib/thespian/actor.rb +24 -35
- data/lib/thespian/dsl.rb +5 -0
- data/lib/thespian/example.rb +41 -0
- data/lib/thespian/strategies/fiber.rb +52 -0
- data/lib/thespian/strategies/interface.rb +35 -0
- data/lib/thespian/strategies/process.rb +73 -0
- data/lib/thespian/strategies/thread.rb +74 -0
- data/lib/thespian/version.rb +1 -1
- data/spec/actor_spec.rb +51 -29
- data/spec/classes_spec.rb +22 -0
- data/spec/spec_helper.rb +21 -19
- data/spec/strategies/fiber_spec.rb +42 -0
- data/spec/strategies/interface.rb +91 -0
- data/spec/strategies/thread_spec.rb +17 -0
- data/spec/thespian_spec.rb +65 -0
- data/thespian.gemspec +6 -2
- metadata +76 -10
data/.travis.yml
ADDED
data/CHANGELOG
CHANGED
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
|
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
data/examples/linked.rb
CHANGED
@@ -1,39 +1,45 @@
|
|
1
1
|
require "thespian"
|
2
|
+
require "thespian/example"
|
2
3
|
|
3
|
-
|
4
|
-
consumer = nil
|
5
|
-
n = 0
|
4
|
+
Thespian::Example.run do
|
6
5
|
|
7
|
-
producer =
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
-
|
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
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
-
|
35
|
-
|
36
|
-
puts consumer.
|
37
|
-
puts
|
38
|
-
puts producer.
|
39
|
-
puts producer.
|
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
|
-
|
4
|
-
consumer = nil
|
4
|
+
Thespian::Example.run do
|
5
5
|
|
6
|
-
producer =
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
-
|
16
|
-
|
17
|
-
puts "consumer got #{message}"
|
24
|
+
producer.start
|
25
|
+
consumer.start
|
18
26
|
producer << :need_item
|
19
|
-
|
27
|
+
while true
|
28
|
+
sleep(1)
|
29
|
+
puts "Total threads: #{Thread.list.length}"
|
30
|
+
end
|
20
31
|
|
21
|
-
|
22
|
-
consumer.start
|
23
|
-
producer << :need_item
|
24
|
-
sleep
|
32
|
+
end
|
data/examples/task_processor.rb
CHANGED
@@ -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.
|
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
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
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
|
data/lib/thespian/actor.rb
CHANGED
@@ -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
|
-
@
|
46
|
-
|
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
|
-
|
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
|
-
@
|
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
|
-
@
|
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
|
176
|
-
@
|
177
|
-
|
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
@@ -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
|