thespian 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ doc
6
+ vendor/bundle
7
+ doc
8
+ .rbenv-version
9
+ .DS_Store
data/CHANGELOG ADDED
@@ -0,0 +1,2 @@
1
+ 0.0.1
2
+ * Initial release
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in thesp.gemspec
4
+ gemspec
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"
@@ -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
@@ -0,0 +1,10 @@
1
+ module Thespian
2
+ class Dsl #:nodoc:
3
+ attr_reader :receive_block
4
+
5
+ def receive(&block)
6
+ @receive_block = block
7
+ end
8
+
9
+ end
10
+ end
@@ -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
@@ -0,0 +1,3 @@
1
+ module Thespian
2
+ VERSION = "0.0.1"
3
+ 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
@@ -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
@@ -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