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 +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
|