Thespian <img src=“https://secure.travis-ci.org/cjbottaro/thespian.png” />
Implementation of the actor pattern built for use with threads and/or fibers.
Quickstart
Dive in…
actor1 = Thespian::Actor.new do ||
sleep() # Simulate work
puts "Actor1 worked for #{} seconds"
end
actor2 = Thespian::Actor.new do ||
sleep() # Simulate work
puts "Actor2 worked for #{} seconds"
end
actor1.start
actor2.start
10.times{ actor1 << rand }
10.times{ actor2 << rand }
actor1.stop
actor2.stop
Unlike other actor APIs, Thespian assumes you want your actor to loop forever, processing messages until it’s told to stop. Thus when you create an actor, all you have to do is specify how to processes messages.
actor = Thespian::Actor.new do ||
# ... handle message ...
end
An actor won’t start processing messages until the Thespian::Actor#start method is called.
actor.start
Also, you cannot put messages into an actor’s mailbox unless it is currently running.
actor << # Will raise an exception if the actor isn't running
You can change this behavior by changing the actor’s strict option.
actor.(:strict => false)
Now you can add messages to the actor’s mailbox, even if it’s not running.
States
An actor can be in one of three states: :initialized
, :running
or :finished
:initialized
is when the actor has been created, but Thespian::Actor#start hasn’t been called (it’s not in the message processing loop yet).
:running
is when #start has been called and it’s processing (or waiting on) messaegs.
:finished
is when the actor is no longer processing messages (it was either instructed to stop or an error occurred).
actor = Thespian::Actor.new{ ... }
actor.state # => :initialized
Error handling
What happens if an actor causes an unhandled exception while processing a message? The actor’s state will be set to :finished
, Thespian::Actor#error? will return true and subsequent calls to various methods will cause the exception to be raised in the calling thread or fiber.
actor = Thespian::Actor.new do ||
raise "oops"
end
actor.start
actor << 1
sleep(0.01)
actor.error? # => true
actor.exception # #<RuntimeError: oops>
actor << 2 # raises #<RuntimeError: oops>
Linking
You can link actors together so that if an error occurs in one, it will trickle up to all that are linked to it.
actor1 = Thespian::Actor.new do ||
puts .inspect
end
actor2 = Thespian::Actor.new do ||
raise "oops"
end
actor1.link(actor2)
actor1.start
actor2.start
actor2 << "blah"
sleep(0.01)
actor1.error? # => true
actor1.exception # => #<Thespian::DeadActorError: oops>
Thespian::DeadActorError contains information about which actor died and the exception that caused it.
Trapping linked errors
If you specify the :trap_exit
option, then instead of raising a Thespian::DeadActorError in the linked actor, it will be put in the actor’s mailbox instead.
actor1 = Thespian::Actor.new do ||
puts .inspect
end
actor2 = Thespian::Actor.new do ||
raise "oops"
end
actor1.(:trap_exit => true)
actor1.link(actor2)
actor1.start
actor2.start
actor2 << "blah"
sleep(0.01)
actor1.error? # => false
actor1.exception # => nil
This will print #<Thespian::DeadActorError: oops>
to stdout.
Classes
You can use Thespian with classes by including the Thespian module into them.
class ArnoldSchwarzenegger
include Thespian
actor.receive do ||
()
end
def ()
puts
end
end
arnold = ArnoldSchwarzenegger.new
arnold.actor.start
arnold.actor << "I'm a cop, you idiot!"
arnold.actor.stop
The block given to actor.receive
on the class is exactly the same as the block given to Thespian::Actor.new except that it is run in the context of an instance of the class (meaning it can call instance methods).
actor
on the instance returns a special instance of Thespian::Actor, that is aware of its relationship to it’s parent object.
What does that mean? Nothing really. The only difference is that Thespian::DeadActorError#actor will return an instance of ArnoldSchwarzenegger instead of an instance of Thespian::Actor.
Fibers
Actors run on threads by default, but you can seamlessly run them on fibers instead:
actor = Thespian::Actor.new(:mode => :fiber){ ... }
Or:
class ArnoldSchwarzenegger
include Thespian
# Any of these will work
actor(:mode => :fiber).receive{ |message| ... }
actor.options[:mode] = :fiber
end
When running in fibered mode, be sure that your actors are executing inside EventMachine’s reactor loop. Also be sure that there is a root fiber. In other words, something like…
EventMachine.run do
Fiber.new do
###
# Your actor code here
###
EventMachine.stop
end.resume
end
Thespian uses Strand behind the scenes which to deal with fibers. Strand is an abstraction that makes fibers behave like threads when used with EventMachine. You will need to install Strand to use Thespian in fibered mode:
gem "strand"
You can change the default mode for all Actors with the following:
Thespian::Actor::DEFAULT_OPTIONS[:mode] = :fiber
RDoc
http://doc.stochasticbytes.com/thespian/index.html
Examples
A simple producer/consumer example.
https://github.com/cjbottaro/thespian/blob/master/examples/producer_consumer.rb
An example where one actor links to another.
https://github.com/cjbottaro/thespian/blob/master/examples/linked.rb
A complex example showing how a background job/task processor could be architected.
https://github.com/cjbottaro/thespian/blob/master/examples/task_processor.rb