/actor

Actor pattern implementation for ruby

Primary LanguageRuby

Actor

Implementation of the actor computational model for ruby.

Basic Usage

Installation

To install actor via rubygems.org, you will have to refer to the gem as ntl-actor when running gem install or adding the gem to Bundler.

gem install ntl-actor

Bundler:

gem 'ntl-actor', require: 'actor'

If you add https://repo.fury.io/ntl/ to your list of gem sources, you can install the library by its proper name:

gem install actor

Bundler:

gem 'actor'

Defining an Actor

class Factorial
  include Actor

  attr_reader :number, :reply_address

  def initialize number, reply_address
    @number, @reply_address = number, reply_address
  end

  handle :start do
    if number == 1
      reply 1
    else
      Factorial.start number - 1, address
    end
  end

  handle :result do |previous_result|
    value = previous_result.value * number

    reply value
  end

  def reply value
    result = Result.new value, number

    send.(result, reply_address)

    :stop
  end

  Result = Struct.new :value, :number do
    include Actor::Messaging::Message
  end
end

Starting an Actor

result_address = Actor::Messaging::Address.build

Factorial.start 42, result_address

result = Actor::Messaging::Read.(result_address)

puts "fac(42) = #{result.value}"

Handling Custom Messages

You can send any ruby object that includes Actor::Messaging::Message to the actor with Actor::Messaging::Send; though mutable objects aren’t recommended, as messages will be read by other threads. Handlers can be defined for those messages through the handle class macro on the Actor class. The class of the message is generally passed to handle, but an underscore cased symbol can be used as well. For example:

class SomeActor
  include Actor

  # ...

  handle :some_message do |message|
    # do something
  end

  handle OtherMessage do |message|
    # do something else
  end
end

# Start an actor and send a custom message to it
address = Actor.start

Actor::Messaging::Send.(SomeMessage.new, address)
Actor::Messaging::Send.(OtherMessage.new, address)

Also, every Actor comes equipped with a send dependency which is just an instance of Actor::Messaging::Send. When any actor is instantiated directly through its initialize method, the send dependency is an inert substitute. When the actor is constructed through the .start class method, the send dependency will actually deliver messages to other actors.

Errors

When an actor raises an error, its thread immediately stops, but the rest of the ruby program remains unaffected. If you call #join on the thread object returned by .start, the error will be re-raised. The actor will not restart itself or deliver an exception notification. When using a supervisor (see below), any errors raised by actors will be re-raised by the supervisor. It should go without saying that errors are undesirable and Actor makes no effort to make them easier to work with. "Don’t let it crash" is the idea.

Supervisor

In production, actors are best run within the context of a supervisor that keeps track of all actors, watches out for crashed actors, and gracefully shuts down actors when it’s time to stop the show. See examples/interactive.rb for an example. Here is a snippet extracted from that file:

Actor::Supervisor.start do |supervisor|
  InteractiveExample::Prompt.start

  Signal.trap 'INT' do
    puts "\n\n** Received SIGINT; shutting down supervisor **\n\n"
    Actor::Messaging::Send.(:shutdown, supervisor.address)
  end
end

The supervisor also publishes all messages it handles via the observer library that ships with ruby. An observer is defined by including Actor::Supervisor::Observer in a class. The handle macro is available to define handlers for any message sent to the supervisor. For instance, the following observer prints out a message whenever an actor is started:

class SomeObserver
  include Actor::Supervisor::Observer

  handle Actor::Messages::ActorStarted do |msg|
    puts "An actor was started: #{msg.address} is its address"
  end
end

Actor::Supervisor.start do |supervisor|
  some_observer = SomeObserver.new

  supervisor.add_observer some_observer

  # Etc.
end

Version Scheme

Actor follows a version scheme with three numbers separated by dots, similar to SemVer, but the numbers have a slightly different meaning. The first number indicates the major product version, or epoch. The second number is increased for breaking changes, otherwise the third number is increased.

License

Actor is licensed under the MIT license

Copyright © Nathan Ladd