thespian 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|