thespian 0.0.1
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/.gitignore +9 -0
- data/CHANGELOG +2 -0
- data/Gemfile +4 -0
- data/README.rdoc +178 -0
- data/Rakefile +1 -0
- data/examples/linked.rb +39 -0
- data/examples/producer_consumer.rb +24 -0
- data/examples/task_processor.rb +170 -0
- data/lib/thespian/actor.rb +245 -0
- data/lib/thespian/dsl.rb +10 -0
- data/lib/thespian/errors.rb +20 -0
- data/lib/thespian/version.rb +3 -0
- data/lib/thespian.rb +46 -0
- data/spec/actor_spec.rb +144 -0
- data/spec/spec_helper.rb +66 -0
- data/thespian.gemspec +26 -0
- metadata +96 -0
data/.gitignore
ADDED
data/CHANGELOG
ADDED
data/Gemfile
ADDED
data/README.rdoc
ADDED
@@ -0,0 +1,178 @@
|
|
1
|
+
== Thespian
|
2
|
+
|
3
|
+
Implementation of the actor pattern built on threads.
|
4
|
+
|
5
|
+
== Quickstart
|
6
|
+
|
7
|
+
Dive in...
|
8
|
+
|
9
|
+
actor1 = Thespian::Actor.new do |message|
|
10
|
+
sleep(message) # Simulate work
|
11
|
+
puts "Actor1 worked for #{message} seconds"
|
12
|
+
end
|
13
|
+
|
14
|
+
actor2 = Thespian::Actor.new do |message|
|
15
|
+
sleep(message) # Simulate work
|
16
|
+
puts "Actor2 worked for #{message} seconds"
|
17
|
+
end
|
18
|
+
|
19
|
+
actor1.start
|
20
|
+
actor2.start
|
21
|
+
|
22
|
+
10.times{ actor1 << rand }
|
23
|
+
10.times{ actor2 << rand }
|
24
|
+
|
25
|
+
actor1.stop
|
26
|
+
actor2.stop
|
27
|
+
|
28
|
+
Unlike other actor APIs, Thespian assumes you want your actor to loop forever, processing messages
|
29
|
+
until it's told to stop. Thus when you create an actor, all you have to do is specify how
|
30
|
+
to processes messages.
|
31
|
+
|
32
|
+
actor = Thespian::Actor.new do |message|
|
33
|
+
# ... handle message ...
|
34
|
+
end
|
35
|
+
|
36
|
+
An actor won't start processing messages until the Thespian::Actor#start method is called.
|
37
|
+
|
38
|
+
actor.start
|
39
|
+
|
40
|
+
Also, you cannot put messages into an actor's mailbox unless it is currently running.
|
41
|
+
|
42
|
+
actor << some_message # Will raise an exception if the actor isn't running
|
43
|
+
|
44
|
+
You can change this behavior by changing the actor's strict option.
|
45
|
+
|
46
|
+
actor.options(:strict => false)
|
47
|
+
|
48
|
+
Now you can add messages to the actor's mailbox, even if it's not running.
|
49
|
+
|
50
|
+
== States
|
51
|
+
|
52
|
+
An actor can be in one of three states: +:initialized+, +:running+ or +:finished+
|
53
|
+
|
54
|
+
+:initialized+ is when the actor has been created, but Thespian::Actor#start hasn't been called (it's not
|
55
|
+
in the message processing loop yet).
|
56
|
+
|
57
|
+
+:running+ is when #start has been called and it's processing (or waiting on) messaegs.
|
58
|
+
|
59
|
+
+:finished+ is when the actor is no longer processing messages (it was either instructed to stop or
|
60
|
+
an error occurred).
|
61
|
+
|
62
|
+
actor = Thespian::Actor.new{ ... }
|
63
|
+
actor.state # => :initialized
|
64
|
+
|
65
|
+
== Error handling
|
66
|
+
|
67
|
+
What happens if an actor causes an unhandled exception while processing a message? The actor's
|
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.
|
70
|
+
|
71
|
+
actor = Thespian::Actor.new do |message|
|
72
|
+
raise "oops"
|
73
|
+
end
|
74
|
+
|
75
|
+
actor.start
|
76
|
+
actor << 1
|
77
|
+
sleep(0.01)
|
78
|
+
actor.error? # => true
|
79
|
+
actor.exception # #<RuntimeError: oops>
|
80
|
+
actor << 2 # raises #<RuntimeError: oops>
|
81
|
+
|
82
|
+
== Linking
|
83
|
+
|
84
|
+
You can link actors together so that if an error occurs in one, it will trickle up to all that
|
85
|
+
are linked to it.
|
86
|
+
|
87
|
+
|
88
|
+
actor1 = Thespian::Actor.new do |message|
|
89
|
+
puts message.inspect
|
90
|
+
end
|
91
|
+
|
92
|
+
actor2 = Thespian::Actor.new do |message|
|
93
|
+
raise "oops"
|
94
|
+
end
|
95
|
+
|
96
|
+
actor1.link(actor2)
|
97
|
+
actor1.start
|
98
|
+
actor2.start
|
99
|
+
|
100
|
+
actor2 << "blah"
|
101
|
+
sleep(0.01)
|
102
|
+
|
103
|
+
actor1.error? # => true
|
104
|
+
actor1.exception # => #<Thespian::DeadActorError: oops>
|
105
|
+
|
106
|
+
Thespian::DeadActorError contains information about which actor died and the exception that caused it.
|
107
|
+
|
108
|
+
== Trapping linked errors
|
109
|
+
|
110
|
+
If you specify the +:trap_exit+ option, then instead of raising a Thespian::DeadActorError in the linked
|
111
|
+
actor, it will be put in the actor's mailbox instead.
|
112
|
+
|
113
|
+
actor1 = Thespian::Actor.new do |message|
|
114
|
+
puts message.inspect
|
115
|
+
end
|
116
|
+
|
117
|
+
actor2 = Thespian::Actor.new do |message|
|
118
|
+
raise "oops"
|
119
|
+
end
|
120
|
+
|
121
|
+
actor1.options(:trap_exit => true)
|
122
|
+
actor1.link(actor2)
|
123
|
+
actor1.start
|
124
|
+
actor2.start
|
125
|
+
|
126
|
+
actor2 << "blah"
|
127
|
+
sleep(0.01)
|
128
|
+
|
129
|
+
actor1.error? # => false
|
130
|
+
actor1.exception # => nil
|
131
|
+
|
132
|
+
This will print <tt>#<Thespian::DeadActorError: oops></tt> to stdout.
|
133
|
+
|
134
|
+
== Classes
|
135
|
+
|
136
|
+
You can use Thespian with classes by including the Thespian module into them.
|
137
|
+
|
138
|
+
class ArnoldSchwarzenegger
|
139
|
+
include Thespian
|
140
|
+
|
141
|
+
actor.receive do |message|
|
142
|
+
handle_message(message)
|
143
|
+
end
|
144
|
+
|
145
|
+
def handle_message(message)
|
146
|
+
puts message
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
arnold = ArnoldSchwarzenegger.new
|
151
|
+
arnold.actor.start
|
152
|
+
arnold.actor << "I'm a cop, you idiot!"
|
153
|
+
arnold.actor.stop
|
154
|
+
|
155
|
+
The block given to +actor.receive+ on the class is exactly the same as the block given to
|
156
|
+
Thespian::Actor.new except that it is run in the context of an instance of the class (meaning it
|
157
|
+
can call instance methods).
|
158
|
+
|
159
|
+
+actor+ on the instance returns a special instance of Thespian::Actor, that is aware of its relationship
|
160
|
+
to it's parent object.
|
161
|
+
|
162
|
+
What does that mean? Nothing really. The only difference is that Thespian::DeadActorError#actor
|
163
|
+
will return an instance of ArnoldSchwarzenegger instead of an instance of Thespian::Actor.
|
164
|
+
|
165
|
+
== RDoc
|
166
|
+
|
167
|
+
{\http://doc.stochasticbytes.com/thespian/index.html}[http://doc.stochasticbytes.com/thespian/index.html]
|
168
|
+
|
169
|
+
== Examples
|
170
|
+
|
171
|
+
A simple producer/consumer example.
|
172
|
+
{\https://github.com/cjbottaro/thespian/blob/master/examples/producer_consumer.rb}[https://github.com/cjbottaro/thespian/blob/master/examples/producer_consumer.rb]
|
173
|
+
|
174
|
+
An example where one actor links to another.
|
175
|
+
{\https://github.com/cjbottaro/thespian/blob/master/examples/linked.rb}[https://github.com/cjbottaro/thespian/blob/master/examples/linked.rb]
|
176
|
+
|
177
|
+
A complex example showing how a background job/task processor could be architected.
|
178
|
+
{\https://github.com/cjbottaro/thespian/blob/master/examples/task_processor.rb}[https://github.com/cjbottaro/thespian/blob/master/examples/task_processor.rb]
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/examples/linked.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require "thespian"
|
2
|
+
|
3
|
+
producer = nil
|
4
|
+
consumer = nil
|
5
|
+
n = 0
|
6
|
+
|
7
|
+
producer = Thespian::Actor.new do |message|
|
8
|
+
case message
|
9
|
+
when :need_item
|
10
|
+
if (n += 1) < 10
|
11
|
+
consumer << rand
|
12
|
+
else
|
13
|
+
consumer << "bad message"
|
14
|
+
end
|
15
|
+
else
|
16
|
+
raise "unexpected message: #{message}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
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!"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
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
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require "thespian"
|
2
|
+
|
3
|
+
producer = nil
|
4
|
+
consumer = nil
|
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}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
consumer = Thespian::Actor.new do |message|
|
16
|
+
sleep(message)
|
17
|
+
puts "consumer got #{message}"
|
18
|
+
producer << :need_item
|
19
|
+
end
|
20
|
+
|
21
|
+
producer.start
|
22
|
+
consumer.start
|
23
|
+
producer << :need_item
|
24
|
+
sleep
|
@@ -0,0 +1,170 @@
|
|
1
|
+
require "thespian"
|
2
|
+
|
3
|
+
# The actors are Supervisor, Logger, Poller, Worker(s). There is one of each except for multiple
|
4
|
+
# workers. The basic idea is that the supervisor can send messages to any other actor, but all
|
5
|
+
# other actors can only send messages to the supervisor. In other words the non-supervisor actors
|
6
|
+
# cannot directly communicate with each other; they have to go through the supervisor.
|
7
|
+
|
8
|
+
# This actor just sits and waits for log messages to print out. It has a 10% chance of erroring
|
9
|
+
# on each message it processes.
|
10
|
+
class Logger
|
11
|
+
include Thespian
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
actor.options(:strict => false)
|
15
|
+
end
|
16
|
+
|
17
|
+
actor.receive do |message|
|
18
|
+
if rand > 0.90
|
19
|
+
raise "logger oops"
|
20
|
+
else
|
21
|
+
puts message
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
# This actor is sent "work" messages. When it's done with the work, it will send a message
|
28
|
+
# with its id back to the supervisor saying that it's ready for more work.
|
29
|
+
# It has a 10% chance of dying while processing a message.
|
30
|
+
class Worker
|
31
|
+
include Thespian
|
32
|
+
attr_reader :id
|
33
|
+
|
34
|
+
def initialize(id, supervisor)
|
35
|
+
@id = id
|
36
|
+
@supervisor = supervisor
|
37
|
+
actor.start
|
38
|
+
end
|
39
|
+
|
40
|
+
actor.receive do |message|
|
41
|
+
raise "oops worker" if rand > 0.90
|
42
|
+
|
43
|
+
cmd, *args = message
|
44
|
+
|
45
|
+
case cmd
|
46
|
+
when :work
|
47
|
+
time, = *args
|
48
|
+
sleep(time)
|
49
|
+
@supervisor.actor << [:log, "worker ##{id} worked for #{time.round(2)} seconds"]
|
50
|
+
@supervisor.actor << [:ready, @id]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
# This actor is responsible for getting "work" for a worker to do. The supervisor will
|
57
|
+
# send it a "get work" message, once it's retreived the work (maybe from a queue if this
|
58
|
+
# was in the real world), it will send it back to the supervisor as "task ready" message.
|
59
|
+
class Poller
|
60
|
+
include Thespian
|
61
|
+
|
62
|
+
def initialize(supervisor)
|
63
|
+
actor.options :strict => false
|
64
|
+
@supervisor = supervisor
|
65
|
+
end
|
66
|
+
|
67
|
+
actor.receive do |cmd, *args|
|
68
|
+
raise "oops poller" if rand > 0.90
|
69
|
+
sleep(0.2)
|
70
|
+
case cmd
|
71
|
+
when :get
|
72
|
+
@supervisor.actor << [:task, rand]
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
# The supervisor is responsible for coordinating all the actors. It's also responsible for
|
79
|
+
# restarting any other actors that die (hence it needs to link to all of them).
|
80
|
+
#
|
81
|
+
# The basic idea is that the supervisor starts up each actor, then keeps a list of idle workers.
|
82
|
+
# For each idle worker, it asks the poller for a task. When the poller responds with a task,
|
83
|
+
# it sends the task to the worker and removes the worker from the idle list. When the worker
|
84
|
+
# reports that it is finished, the worker is placed back in the idle list and the supervisor
|
85
|
+
# asks the poller for another task.
|
86
|
+
class Supervisor
|
87
|
+
include Thespian
|
88
|
+
|
89
|
+
def initialize(count)
|
90
|
+
@ready = []
|
91
|
+
actor.options(:trap_exit => true)
|
92
|
+
actor.start
|
93
|
+
initialize_logger
|
94
|
+
initialize_poller
|
95
|
+
count.times{ |id| initialize_worker(id) }
|
96
|
+
end
|
97
|
+
|
98
|
+
actor.receive do |message|
|
99
|
+
case message
|
100
|
+
when Thespian::DeadActorError
|
101
|
+
handle_dead_actor(message.actor)
|
102
|
+
when Array
|
103
|
+
cmd, *args = message
|
104
|
+
send("do_#{cmd}", *args)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def handle_dead_actor(actor)
|
109
|
+
case actor
|
110
|
+
when Logger
|
111
|
+
do_log("logger died, restarting")
|
112
|
+
initialize_logger
|
113
|
+
when Worker
|
114
|
+
do_log("worker(#{actor.id}) died, restarting")
|
115
|
+
initialize_worker(actor.id)
|
116
|
+
when Poller
|
117
|
+
do_log("poller died (#{@ready.length}, #{@poller.actor.instance_eval{ @mailbox }.length}), restarting")
|
118
|
+
initialize_poller
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def do_log(message)
|
123
|
+
@logger.actor << message
|
124
|
+
end
|
125
|
+
|
126
|
+
def do_ready(id)
|
127
|
+
@ready << id
|
128
|
+
@poller.actor << [:get]
|
129
|
+
end
|
130
|
+
|
131
|
+
def do_task(arg)
|
132
|
+
id = @ready.shift
|
133
|
+
@workers[id].actor << [:work, arg]
|
134
|
+
end
|
135
|
+
|
136
|
+
def initialize_logger
|
137
|
+
if @logger
|
138
|
+
old_mailbox = @logger.actor.salvage_mailbox
|
139
|
+
end
|
140
|
+
@logger = Logger.new
|
141
|
+
actor.link(@logger)
|
142
|
+
if old_mailbox
|
143
|
+
old_mailbox.each{ |message| @logger.actor << message }
|
144
|
+
end
|
145
|
+
@logger.actor.start
|
146
|
+
end
|
147
|
+
|
148
|
+
def initialize_poller
|
149
|
+
@poller = Poller.new(self)
|
150
|
+
@ready.each{ @poller.actor << [:get] }
|
151
|
+
actor.link(@poller)
|
152
|
+
@poller.actor.start
|
153
|
+
end
|
154
|
+
|
155
|
+
def initialize_worker(id)
|
156
|
+
@workers ||= []
|
157
|
+
@workers[id] = Worker.new(id, self).tap do |worker|
|
158
|
+
@ready << id
|
159
|
+
actor.link(worker)
|
160
|
+
@poller.actor << [:get]
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
165
|
+
|
166
|
+
s = Supervisor.new(5)
|
167
|
+
while true
|
168
|
+
raise s.actor.exception unless s.actor.running?
|
169
|
+
sleep(1)
|
170
|
+
end
|
@@ -0,0 +1,245 @@
|
|
1
|
+
require "thread"
|
2
|
+
require "monitor"
|
3
|
+
require "set"
|
4
|
+
|
5
|
+
require "thespian/errors"
|
6
|
+
|
7
|
+
module Thespian
|
8
|
+
class Actor
|
9
|
+
|
10
|
+
# If an actor died due to an exception, it is stored here.
|
11
|
+
attr_reader :exception
|
12
|
+
|
13
|
+
# Returns the actor's state.
|
14
|
+
attr_reader :state
|
15
|
+
|
16
|
+
# The state an actor is in after it has been created, but before it enters the message processing loop.
|
17
|
+
STATE_INITIALIZED = :initialized
|
18
|
+
|
19
|
+
# The state for when an actor is in the message processing loop.
|
20
|
+
STATE_RUNNING = :running
|
21
|
+
|
22
|
+
# The state for when an actor has exited the message processing loop (either by error or intentially).
|
23
|
+
STATE_FINISHED = :finished
|
24
|
+
|
25
|
+
DEFAULT_OPTIONS = {
|
26
|
+
:strict => true,
|
27
|
+
:trap_exit => false,
|
28
|
+
:object => nil
|
29
|
+
} #:nodoc:
|
30
|
+
|
31
|
+
# call-seq:
|
32
|
+
# new(options = {}){ |message| ... }
|
33
|
+
#
|
34
|
+
# Create a new actor using the given block to handles messages. The actor will be in the +:initialized+
|
35
|
+
# state, meaning the message processing loop hasn't started yet (see #start).
|
36
|
+
#
|
37
|
+
# +options+ are the same as specified by #options.
|
38
|
+
def initialize(options = {}, &block)
|
39
|
+
self.options(DEFAULT_OPTIONS.merge(options))
|
40
|
+
|
41
|
+
@state = :initialized
|
42
|
+
@receive_block = block
|
43
|
+
@linked_actors = Set.new
|
44
|
+
|
45
|
+
@mailbox = []
|
46
|
+
@mailbox_lock = Monitor.new
|
47
|
+
@mailbox_cond = @mailbox_lock.new_cond
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
# call-seq:
|
52
|
+
# options -> Hash
|
53
|
+
# options(hash) -> Hash
|
54
|
+
# options(symbol) -> value
|
55
|
+
#
|
56
|
+
# Get or set options. Valid options are:
|
57
|
+
# [:strict (default true)]
|
58
|
+
# Require an actor to be running in order to put messages into its mailbox.
|
59
|
+
# [:trap_exit (default false)]
|
60
|
+
# If true, the actor will get a DeadActorError message in its mailbox when a linked actor raises an unhandled exception.
|
61
|
+
#
|
62
|
+
# If given no arguments, returns a hash of options.
|
63
|
+
#
|
64
|
+
# If given a hash, sets the options specified in the hash.
|
65
|
+
#
|
66
|
+
# If given a symbol, returns that option's value.
|
67
|
+
def options(arg = nil)
|
68
|
+
@options ||= {}
|
69
|
+
|
70
|
+
case arg
|
71
|
+
when Hash
|
72
|
+
@options.merge!(arg)
|
73
|
+
when Symbol
|
74
|
+
@options[arg]
|
75
|
+
when nil
|
76
|
+
@options
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# call-seq:
|
81
|
+
# link(actor)
|
82
|
+
#
|
83
|
+
# Exceptions in actors will be propogated to actors that are linked to them. How they are propogated is
|
84
|
+
# determined by the +:trap_exit+ option (see #options).
|
85
|
+
#
|
86
|
+
# +actor+ can either be an Actor instance or an instance of a class that included Thespian.
|
87
|
+
def link(object)
|
88
|
+
actor = object.kind_of?(Actor) ? object : object.actor
|
89
|
+
actor.send(:_link, self)
|
90
|
+
object
|
91
|
+
end
|
92
|
+
|
93
|
+
# Start the actor's message processing loop.
|
94
|
+
# The thread that the loop is run on is guaranteed to have started by the time this method returns.
|
95
|
+
def start
|
96
|
+
|
97
|
+
# Don't let them start an already started actor.
|
98
|
+
raise "already running" if running?
|
99
|
+
|
100
|
+
# Can't raise an actor from the dead.
|
101
|
+
raise @exception if @exception
|
102
|
+
|
103
|
+
# IMPORTANT - Race condition!
|
104
|
+
# This method and the thread both set @state. We don't want this method to
|
105
|
+
# possibly overwrite how the thread sets @state, so we set the @state
|
106
|
+
# before staring the thread.
|
107
|
+
@state = :running
|
108
|
+
|
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
|
128
|
+
end
|
129
|
+
|
130
|
+
# Add a message to the actor's mailbox.
|
131
|
+
# May raise an exception according to #check_alive!
|
132
|
+
def <<(message)
|
133
|
+
check_alive! if options(:strict)
|
134
|
+
message = message.new if message == Stop
|
135
|
+
@mailbox_lock.synchronize do
|
136
|
+
@mailbox << message
|
137
|
+
@mailbox_cond.signal
|
138
|
+
end
|
139
|
+
self
|
140
|
+
end
|
141
|
+
|
142
|
+
# Stop the actor.
|
143
|
+
# All pending messages will be processed before stopping the actor.
|
144
|
+
# Raises an exception if the actor is not #running? and +:strict+ (see #options) is true.
|
145
|
+
def stop
|
146
|
+
check_alive! if options(:strict)
|
147
|
+
self << Stop.new
|
148
|
+
@thread.join
|
149
|
+
raise @exception if @exception
|
150
|
+
end
|
151
|
+
|
152
|
+
# #state == :initialized
|
153
|
+
def initialized?
|
154
|
+
state == :initialized
|
155
|
+
end
|
156
|
+
|
157
|
+
# #state == :running
|
158
|
+
def running?
|
159
|
+
state == :running
|
160
|
+
end
|
161
|
+
|
162
|
+
# #state == :finished
|
163
|
+
def finished?
|
164
|
+
state == :finished
|
165
|
+
end
|
166
|
+
|
167
|
+
# Returns true if an error occurred that caused the actor to enter the :finished state.
|
168
|
+
def error?
|
169
|
+
!!@exception
|
170
|
+
end
|
171
|
+
|
172
|
+
# Salvage mailbox contents from a dead actor (including the message it died on).
|
173
|
+
# Useful for restarting a dead actor while preserving its mailbox.
|
174
|
+
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
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
private
|
182
|
+
|
183
|
+
# This wraps the user's #act method.
|
184
|
+
# It traps exceptions and notifies any linked actors.
|
185
|
+
def run
|
186
|
+
if options(:object)
|
187
|
+
loop{ options(:object).instance_exec(receive, &@receive_block) }
|
188
|
+
elsif @receive_block
|
189
|
+
loop{ @receive_block.call(receive) }
|
190
|
+
else
|
191
|
+
loop{ receive }
|
192
|
+
end
|
193
|
+
rescue Stop
|
194
|
+
nil
|
195
|
+
rescue Exception => e
|
196
|
+
@exception = e
|
197
|
+
@last_message = @message
|
198
|
+
ensure
|
199
|
+
@state = :finished
|
200
|
+
notify_linked_of(@exception) if @exception
|
201
|
+
end
|
202
|
+
|
203
|
+
# Receive a message from the actor's mailbox.
|
204
|
+
def receive
|
205
|
+
message = @mailbox_lock.synchronize do
|
206
|
+
@mailbox_cond.wait_while{ @mailbox.empty? }
|
207
|
+
@message = @mailbox.shift
|
208
|
+
end
|
209
|
+
|
210
|
+
# Communicate with #run by possibly raising exceptions here.
|
211
|
+
case message
|
212
|
+
when DeadActorError
|
213
|
+
raise message unless options(:trap_exit)
|
214
|
+
when Stop
|
215
|
+
raise message
|
216
|
+
end
|
217
|
+
|
218
|
+
message # Return the message
|
219
|
+
end
|
220
|
+
|
221
|
+
# The other side of #link.
|
222
|
+
def _link(actor)
|
223
|
+
@linked_actors << actor
|
224
|
+
end
|
225
|
+
|
226
|
+
# Notifies all linked actors of the given exception, by adding it to their mailboxes.
|
227
|
+
def notify_linked_of(exception)
|
228
|
+
@linked_actors.each do |actor|
|
229
|
+
actor << DeadActorError.new(options(:object) || self, exception)
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# An exception will be raised if the actor is not alive (is not running).
|
234
|
+
# It will be a DeadActorError if died due to a linked actor, otherwise it will be a RuntimeError.
|
235
|
+
def check_alive!
|
236
|
+
return if running?
|
237
|
+
if @exception
|
238
|
+
raise @exception
|
239
|
+
else
|
240
|
+
raise RuntimeError, "actor is not running"
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
end
|
245
|
+
end
|
data/lib/thespian/dsl.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
module Thespian
|
2
|
+
class DeadActorError < RuntimeError
|
3
|
+
|
4
|
+
# The actor or host object that died.
|
5
|
+
attr_reader :actor
|
6
|
+
|
7
|
+
# The exact exception that caused the actor to die.
|
8
|
+
attr_reader :reason
|
9
|
+
|
10
|
+
def initialize(actor, exception) #:nodoc:
|
11
|
+
super(exception)
|
12
|
+
@actor = actor
|
13
|
+
@reason = exception
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# This is used as the stop message.
|
18
|
+
class Stop < StandardError #:nodoc:
|
19
|
+
end
|
20
|
+
end
|
data/lib/thespian.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require "thespian/actor"
|
2
|
+
require "thespian/dsl"
|
3
|
+
require "thespian/version"
|
4
|
+
|
5
|
+
# Include this module into classes to give them acting abilities.
|
6
|
+
# class ArnoldSchwarzenegger
|
7
|
+
# include Thespian
|
8
|
+
#
|
9
|
+
# actor.receive do |message|
|
10
|
+
# handle_message(message)
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# def handle_message(message)
|
14
|
+
# puts message
|
15
|
+
# end
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# arnold = ArnoldSchwarzenegger.new
|
19
|
+
# arnold.actor.start
|
20
|
+
# arnold.actor << "I'm a cop, you idiot!"
|
21
|
+
# arnold.actor.stop
|
22
|
+
#
|
23
|
+
# For a general overview of this gem, see the README.rdoc.
|
24
|
+
module Thespian
|
25
|
+
|
26
|
+
def self.included(mod) #:nodoc:
|
27
|
+
mod.send(:extend, ClassMethods)
|
28
|
+
mod.send(:include, InstanceMethods)
|
29
|
+
end
|
30
|
+
|
31
|
+
module ClassMethods #:nodoc:
|
32
|
+
|
33
|
+
def actor
|
34
|
+
@actor ||= Dsl.new
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
module InstanceMethods #:nodoc:
|
40
|
+
|
41
|
+
def actor
|
42
|
+
@actor ||= Actor.new(:object => self, &self.class.actor.receive_block)
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
data/spec/actor_spec.rb
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module Thespian
|
4
|
+
describe Actor do
|
5
|
+
|
6
|
+
context "#new" do
|
7
|
+
let(:actor){ Actor.new }
|
8
|
+
|
9
|
+
it "returns a new Actor" do
|
10
|
+
actor.should be_a(Actor)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "that is initialized" do
|
14
|
+
actor.should be_initialized
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
context "#link" do
|
19
|
+
let(:actor1){ Actor.new.extend(ActorHelper) }
|
20
|
+
let(:actor2){ Actor.new.extend(ActorHelper) }
|
21
|
+
|
22
|
+
it "adds self to the other actor's linked list" do
|
23
|
+
actor1.link(actor2)
|
24
|
+
actor2.linked_actors.should include(actor1)
|
25
|
+
actor1.linked_actors.should_not include(actor2)
|
26
|
+
end
|
27
|
+
|
28
|
+
it "can take host objects as argument" do
|
29
|
+
host = Class.new{ include Thespian }.new
|
30
|
+
host.actor.extend(ActorHelper)
|
31
|
+
|
32
|
+
actor1.link(host)
|
33
|
+
host.actor.linked_actors.should include(actor1)
|
34
|
+
actor1.linked_actors.should_not include(host.actor)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
context "#start" do
|
39
|
+
let(:actor){ Actor.new.extend(ActorHelper) }
|
40
|
+
|
41
|
+
before(:all) do
|
42
|
+
actor.start
|
43
|
+
end
|
44
|
+
|
45
|
+
it "marks the actor as alive" do
|
46
|
+
actor.should be_running
|
47
|
+
end
|
48
|
+
|
49
|
+
it "starts a thread" do
|
50
|
+
actor.thread.should be_alive
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
context "#receive" do
|
55
|
+
let(:actor){ Actor.new.extend(ActorHelper) }
|
56
|
+
|
57
|
+
it "returns the next message from the actor's mailbox" do
|
58
|
+
actor.mailbox << "hello"
|
59
|
+
actor.receive.should == "hello"
|
60
|
+
end
|
61
|
+
|
62
|
+
it "raises a DeadActorError if that's what's in the mailbox" do
|
63
|
+
actor.mailbox << DeadActorError.new(actor, "blah")
|
64
|
+
expect{ actor.receive }.to raise_error(DeadActorError)
|
65
|
+
end
|
66
|
+
|
67
|
+
it "raises a Stop exception if that's what's in the mailbox" do
|
68
|
+
actor.mailbox << Stop.new
|
69
|
+
expect{ actor.receive }.to raise_error(Stop)
|
70
|
+
end
|
71
|
+
|
72
|
+
it "returns DeadActorError if trap_exit is true and that's what's in the mailbox" do
|
73
|
+
actor.mailbox << DeadActorError.new(actor, "blah")
|
74
|
+
actor.options(:trap_exit => true)
|
75
|
+
actor.receive.should be_a(DeadActorError)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
context "#<<" do
|
80
|
+
let(:actor){ Actor.new.extend(ActorHelper) }
|
81
|
+
|
82
|
+
it "puts an item into the mailbox" do
|
83
|
+
stub(actor).running?{ true }
|
84
|
+
actor << "hello"
|
85
|
+
actor.mailbox.should include("hello")
|
86
|
+
end
|
87
|
+
|
88
|
+
it "raises a RuntimeError if the actor isn't alive" do
|
89
|
+
actor.should_not be_running
|
90
|
+
expect{ actor << "hello" }.to raise_error(RuntimeError, /not running/i)
|
91
|
+
end
|
92
|
+
|
93
|
+
it "works on a dead actor if strict is false" do
|
94
|
+
actor.should_not be_running
|
95
|
+
actor.options :strict => false
|
96
|
+
actor << "blah"
|
97
|
+
actor.mailbox.should include("blah")
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
context "#stop" do
|
102
|
+
let(:actor){ Actor.new.extend(ActorHelper) }
|
103
|
+
|
104
|
+
it "raises an exception if the actor isn't alive" do
|
105
|
+
expect{ actor.stop }.to raise_error(RuntimeError, /not running/i)
|
106
|
+
end
|
107
|
+
|
108
|
+
it "puts a Stop message in the actor's mailbox" do
|
109
|
+
mock(actor).running?{ true }.times(2)
|
110
|
+
mock(actor.thread).join
|
111
|
+
actor.stop
|
112
|
+
actor.mailbox[0].should be_a(Stop)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
context "#salvage_mailbox" do
|
117
|
+
|
118
|
+
it "raises an error if the actor isn't done" do
|
119
|
+
actor = Actor.new
|
120
|
+
expect{ actor.salvage_mailbox }.to raise_error(/isn't finished/i)
|
121
|
+
end
|
122
|
+
|
123
|
+
it "doesn't include the last message if the actor stopped properly" do
|
124
|
+
actor = Actor.new.extend(ActorHelper)
|
125
|
+
actor.mailbox.replace([1, 2, 3, Stop.new, 4, 5])
|
126
|
+
actor.start
|
127
|
+
Thread.pass while actor.running?
|
128
|
+
actor.salvage_mailbox.should == [4, 5]
|
129
|
+
end
|
130
|
+
|
131
|
+
it "includes the last message if the actor error'ed" do
|
132
|
+
actor = Actor.new do |message|
|
133
|
+
raise "oops" if message == 3
|
134
|
+
end.extend(ActorHelper)
|
135
|
+
actor.mailbox.replace([1, 2, 3, 4, 5])
|
136
|
+
actor.start
|
137
|
+
Thread.pass while actor.running?
|
138
|
+
actor.salvage_mailbox.should == [3, 4, 5]
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
144
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
require "thespian"
|
2
|
+
|
3
|
+
module ActorHelper
|
4
|
+
|
5
|
+
def linked_actors
|
6
|
+
@linked_actors
|
7
|
+
end
|
8
|
+
|
9
|
+
def thread
|
10
|
+
@thread
|
11
|
+
end
|
12
|
+
|
13
|
+
def mailbox
|
14
|
+
@mailbox
|
15
|
+
end
|
16
|
+
|
17
|
+
def mailbox_cond
|
18
|
+
@mailbox_cond
|
19
|
+
end
|
20
|
+
|
21
|
+
# To get around receive being private
|
22
|
+
def receive
|
23
|
+
super
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
require 'rr'
|
29
|
+
|
30
|
+
module RR
|
31
|
+
module Adapters
|
32
|
+
module RSpec2
|
33
|
+
|
34
|
+
include RRMethods
|
35
|
+
|
36
|
+
def setup_mocks_for_rspec
|
37
|
+
RR.reset
|
38
|
+
end
|
39
|
+
def verify_mocks_for_rspec
|
40
|
+
RR.verify
|
41
|
+
end
|
42
|
+
def teardown_mocks_for_rspec
|
43
|
+
RR.reset
|
44
|
+
end
|
45
|
+
|
46
|
+
def have_received(method = nil)
|
47
|
+
RR::Adapters::Rspec::InvocationMatcher.new(method)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
RSpec.configuration.backtrace_clean_patterns.push(RR::Errors::BACKTRACE_IDENTIFIER)
|
54
|
+
|
55
|
+
module RSpec
|
56
|
+
module Core
|
57
|
+
module MockFrameworkAdapter
|
58
|
+
include RR::Adapters::RSpec2
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
RSpec.configure do |config|
|
64
|
+
config.mock_with :rr
|
65
|
+
end
|
66
|
+
|
data/thespian.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "thespian/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "thespian"
|
7
|
+
s.version = Thespian::VERSION
|
8
|
+
s.authors = ["Christopher J. Bottaro"]
|
9
|
+
s.email = ["cjbottaro@alumni.cs.utexas.edu"]
|
10
|
+
s.homepage = "https://github.com/cjbottaro/thespian"
|
11
|
+
s.summary = %q{Implementation of actor pattern using threads}
|
12
|
+
s.description = %q{Ruby implementation of actor pattern built on threads}
|
13
|
+
|
14
|
+
s.rubyforge_project = "thespian"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
# specify any dependencies here; for example:
|
22
|
+
s.add_development_dependency "rspec"
|
23
|
+
s.add_development_dependency "rr"
|
24
|
+
s.add_development_dependency "rdoc"
|
25
|
+
# s.add_runtime_dependency "rest-client"
|
26
|
+
end
|
metadata
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: thespian
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Christopher J. Bottaro
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-01-17 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: &70140461192260 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70140461192260
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: rr
|
27
|
+
requirement: &70140461191780 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70140461191780
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: rdoc
|
38
|
+
requirement: &70140461191300 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70140461191300
|
47
|
+
description: Ruby implementation of actor pattern built on threads
|
48
|
+
email:
|
49
|
+
- cjbottaro@alumni.cs.utexas.edu
|
50
|
+
executables: []
|
51
|
+
extensions: []
|
52
|
+
extra_rdoc_files: []
|
53
|
+
files:
|
54
|
+
- .gitignore
|
55
|
+
- CHANGELOG
|
56
|
+
- Gemfile
|
57
|
+
- README.rdoc
|
58
|
+
- Rakefile
|
59
|
+
- examples/linked.rb
|
60
|
+
- examples/producer_consumer.rb
|
61
|
+
- examples/task_processor.rb
|
62
|
+
- lib/thespian.rb
|
63
|
+
- lib/thespian/actor.rb
|
64
|
+
- lib/thespian/dsl.rb
|
65
|
+
- lib/thespian/errors.rb
|
66
|
+
- lib/thespian/version.rb
|
67
|
+
- spec/actor_spec.rb
|
68
|
+
- spec/spec_helper.rb
|
69
|
+
- thespian.gemspec
|
70
|
+
homepage: https://github.com/cjbottaro/thespian
|
71
|
+
licenses: []
|
72
|
+
post_install_message:
|
73
|
+
rdoc_options: []
|
74
|
+
require_paths:
|
75
|
+
- lib
|
76
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
77
|
+
none: false
|
78
|
+
requirements:
|
79
|
+
- - ! '>='
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ! '>='
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
requirements: []
|
89
|
+
rubyforge_project: thespian
|
90
|
+
rubygems_version: 1.8.11
|
91
|
+
signing_key:
|
92
|
+
specification_version: 3
|
93
|
+
summary: Implementation of actor pattern using threads
|
94
|
+
test_files:
|
95
|
+
- spec/actor_spec.rb
|
96
|
+
- spec/spec_helper.rb
|