Project

thespian

0.0
No commit activity in last 3 years
No release in over 3 years
Ruby implementation of actor pattern for use with threads and/or fibers
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

>= 0
>= 0
>= 0
>= 0
>= 0
>= 0
 Project Readme

Implementation of the actor pattern built for use with threads and/or fibers.

Quickstart¶ ↑

Dive in…

actor1 = Thespian::Actor.new do |message|
  sleep(message) # Simulate work
  puts "Actor1 worked for #{message} seconds"
end

actor2 = Thespian::Actor.new do |message|
  sleep(message) # Simulate work
  puts "Actor2 worked for #{message} 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 |message|
  # ... 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 << some_message # Will raise an exception if the actor isn't running

You can change this behavior by changing the actor’s strict option.

actor.options(: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 |message|
  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 |message|
  puts message.inspect
end

actor2 = Thespian::Actor.new do |message|
  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 |message|
  puts message.inspect
end

actor2 = Thespian::Actor.new do |message|
  raise "oops"
end

actor1.options(: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 |message|
    handle_message(message)
  end

  def handle_message(message)
    puts message
  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

  # This will work
  actor(:mode => :fiber).receive{ |message| ... }

  # Or this for short
  actor(:mode => :fiber){ |message| ... }

  # Or even this
  actor.options[:mode] = :fiber
  actor{ |message| ... }
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 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