#968 Actors - best practices?

ivan Wed 10 Feb 2010

Last few months I use Actors quite intensive, and found approach which works good for myself. So I'd like to share it and know what others think about it.

Let's consider the simple Counter actor, which has unlimited number of counters identified by name. So, I define an actor class like this:

const class CounterActor : Actor
{
  //////////////////////////////////////////////////////////////////////////
  // Constructor
  //////////////////////////////////////////////////////////////////////////
  new make(ActorPool pool := ActorPool()) : super(pool) {}

Locals-backed properties:

//////////////////////////////////////////////////////////////////////////
// Actor.locals-backed properties
//////////////////////////////////////////////////////////////////////////
private Str:Int counters
{
  get { locals.getOrAdd("counters") |->Obj| { [:] } }
  set { locals["counters"] = it } //setter just for illustration, not needed here
}

Public API (actor just sends message to self):

//////////////////////////////////////////////////////////////////////////
// Public API
//////////////////////////////////////////////////////////////////////////
Str:Int getAll() { send(GetCounters.instance).get  }

Int get(Str name) { getAll[name] }

Void set(Str name, Int value) { send(SetCounter(name, value)).get}

Void incr(Str name, Int value := 1) { send(IncrCounter(name, value)) }

Void reset() { send(ClearAll.instance) }

Receive method:

//////////////////////////////////////////////////////////////////////////
// Overriden methods
//////////////////////////////////////////////////////////////////////////
override Obj? receive(Obj? msg)
{
  if(msg is GetCounters) return processGet
  else if(msg is ClearAll) processClear
  else if(msg is IncrCounter) processIncr(msg)
  else if(msg is SetCounter) processSet(msg)
  return null
}

Message processors:

//////////////////////////////////////////////////////////////////////////
// Message handlers 
//////////////////////////////////////////////////////////////////////////
private Void processClear() { counters.clear }

private Void processSet(SetCounter msg) { msg { counters[key] = val } }

private Str:Int processGet() { counters }

private Void processIncr(IncrCounter msg) 
{ 
  msg { counters[key] = (counters[key] ?:0) + val } 
}

Internal message classes (quite trivial):

internal const class GetCounters
{
  private new make() {}
  const static GetCounters instance := GetCounters()
}
internal const class SetCounter
{
  const Str key
  const Int val

  new make(Str key, Int val := 0)
  {
    this.key = key
    this.val = val
  }

}

internal const class IncrCounter
{
  new make(Str key, Int val := 1)
  {
    this.key = key
    this.val = val
  }
  const Str key
  const Int val
}

internal const class ClearAll
{
  private new make() {}
  const static ClearAll instance := ClearAll()
}

After that, usage is very straightforward:

counter := CounterActor()
counter.incr("foo", 5)
counter.set("bar", 4)
echo(counter["foo"])
echo(counter["bar"])

Few notes:

  • I often expose the Public API part as mixin.
  • When necessary, it is still possible to return Future from API method or add Bool sync := false parameter
  • For particular counter actor this example may look too heavy, but in more complex cases the usage convenience overweights boilerplate code from my point of view

jodastephen Wed 10 Feb 2010

Thanks for the writeup.

My reaction is what a lot of code!

Firstly, the message classes demonstrate why we need simple state/bean classes ( SetCounter, IncrCounter), such as state class. `http://fantom.org/sidewalk/topic/378`

internal const state class SetCounter {
  const Str key
  const Int val := 0
}

I'd also suggest that there should be an easy way to create singletons ( GetCounters, CountAll), either by making the default constructor private or adding singleton class types. `http://fantom.org/sidewalk/topic/404`

internal const singleton class ClearAll {
}

And perhaps the properties stored in locals should be genuine fields as far as the code looks, but in an actor class type:

const actor class CounterActor {
  new make(ActorPool pool := ActorPool()) : super(pool) {}
  private Str:Int counters = [:]
  ...

Overall however, I still think that being able to define shared concurrent variables as an alternative to actors is useful. This example is just AtomicInteger in Java and doesn't need a complex actor/threaded backend - the same is true for ConcurrentHashMap. As constructs, the Java concurrent classes will be a lot easier for newcomers, and avoid forcing new starters to learn actors, which is a good thing.

brian Wed 10 Feb 2010

Nice write-up ivan.

A technique I use myself a lot is to just use a list as tuple with the first item being the dispatch key:

Obj foo(Str a) { send(["doFoo", a]).get }
Obj bar(Int a, Int b) { send(["doBar", a, b].get }

Sometimes, I even just take that and dispatch with reflection internally:

Obj receive(Obj[] msg)
{
  field(msg.first).callOn(this, msg[1..-1])
}

Obj doFoo(Str a) { ... }

Obj doBar(Int a, Int b) { ... }

Another technique I use is to perform error checking on the caller's thread before I send the message to the actor for processing.

Of course there is lots of opportunities for building libraries and frameworks on top of the core. Key thing is to ensure our foundation is solid.

ivan Wed 10 Feb 2010

Thanks brian!

Very nice technique!

One of possible variations:

override Obj? receive(Obj? msg) { callOn(msg) }

private Obj? callOn(Obj?[] msg) { msg.first->callOn(this, msg[1..-1]) }

Void something(Str a, Int b) { send([#doSomething, a, b].toImmutable) }

private Void doSomething(Str a, Int b) { ... }

ivan Fri 2 Jul 2010

Going back to this topic - I wrote a base class which I use extensively for my actors now. Example:

const class MyActor : ReflectActor
{
  new make(ActorPool pool := ActorPool()) : super(pool) {}
  //////////////////////////////////////////////////////////////////////////
  // Public API
  //////////////////////////////////////////////////////////////////////////    
  Void someOp() { send([#doSomeOp, [,]].toImmutable) }
  Future getSum(Int arg1, Int arg2)
  {
    send([#doGetSomething, [arg1, arg2]].toImmutable)
  }

  Int getSumNow(Int arg1, Int arg2) { getSum(arg1, arg2).get }
  //////////////////////////////////////////////////////////////////////////
  // Message handlers
  //////////////////////////////////////////////////////////////////////////
  protected Int doGetSum(Int a, Int b) { a + b }
  protected Void doSomeOp() {}
}

ReflectActor:

const class ReflectActor : Actor
{
  new make(ActorPool pool) : super(pool) {}

  override Obj? receive(Obj? msg)
  {
    if(msg == null) throw ArgErr("Null messages are not supported")
    if(msg isnot Obj[]) throw ArgErr("Unsupported message type $msg.typeof")
    list := msg as Obj?[]
    method := list.first as Method
    args := list.last as Obj?[]
    if(list.size != 2 || method == null
      || args == null) 
      throw ArgErr("List must have two elements - method and list of args")
    return method.callOn(this, args)
  }
}

rfeldman Fri 2 Jul 2010

Huh. So this sort of gives an Actor the feel of a Java Thread that takes a Runnable.

Login or Signup to reply.