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