on demand actor get or else create

2019-01-23 02:57发布

问题:

I can create actors with actorOf and look them with actorFor. I now want to get an actor by some id:String and if it doesnt exist, I want it to be created. Something like this:

  def getRCActor(id: String):ActorRef = {
    Logger.info("getting actor %s".format(id))
    var a = system.actorFor(id)
    if(a.isTerminated){
      Logger.info("actor is terminated, creating new one")
      return system.actorOf(Props[RC], id:String)
    }else{
      return a
    }
   }

But this doesn't work as isTerminated is always true and I get actor name 1 is not unique! exception for the second call. I guess I am using the wrong pattern here. Can someone help how to achieve this? I need

  • Create actors on demand
  • Lookup actors by id and if not present create them
  • Ability to destroy on, as I don't know if I will need it again

Should I use a Dispatcher or Router for this?

Solution As proposed I use a concrete Supervisor that holds the available actors in a map. It can be asked to provide one of his children.

class RCSupervisor extends Actor {

  implicit val timeout = Timeout(1 second)
  var as = Map.empty[String, ActorRef]

  def getRCActor(id: String) = as get id getOrElse {
    val c = context actorOf Props[RC]
    as += id -> c
    context watch c
    Logger.info("created actor")
    c
  }

  def receive = {

    case Find(id) => {
      sender ! getRCActor(id)
    }

    case Terminated(ref) => {
      Logger.info("actor terminated")
      as = as filterNot { case (_, v) => v == ref }
    }
  }
}

His companion object

object RCSupervisor {

  // this is specific to Playframework (Play's default actor system)
  var supervisor = Akka.system.actorOf(Props[RCSupervisor])

  implicit val timeout = Timeout(1 second)

  def findA(id: String): ActorRef = {
    val f = (supervisor ? Find(id))
    Await.result(f, timeout.duration).asInstanceOf[ActorRef]
  }
  ...
}

回答1:

I've not been using akka for that long, but the creator of the actors is by default their supervisor. Hence the parent can listen for their termination;

var as = Map.empty[String, ActorRef] 
def getRCActor(id: String) = as get id getOrElse {
  val c = context actorOf Props[RC]
  as += id -> c
  context watch c
  c
}

But obviously you need to watch for their Termination;

def receive = {
  case Terminated(ref) => as = as filterNot { case (_, v) => v == ref }

Is that a solution? I must say I didn't completely understand what you meant by "terminated is always true => actor name 1 is not unique!"



回答2:

Actors can only be created by their parent, and from your description I assume that you are trying to have the system create a non-toplevel actor, which will always fail. What you should do is to send a message to the parent saying “give me that child here”, then the parent can check whether that currently exists, is in good health, etc., possibly create a new one and then respond with an appropriate result message.

To reiterate this extremely important point: get-or-create can ONLY ever be done by the direct parent.



回答3:

I based my solution to this problem on oxbow_lakes' code/suggestion, but instead of creating a simple collection of all the children actors I used a (bidirectional) map, which might be beneficial if the number of child actors is significant.

import play.api._
import akka.actor._
import scala.collection.mutable.Map 

trait ResponsibleActor[K] extends Actor {
  val keyActorRefMap: Map[K, ActorRef] = Map[K, ActorRef]()
  val actorRefKeyMap: Map[ActorRef, K] = Map[ActorRef, K]()

  def getOrCreateActor(key: K, props: => Props, name: => String): ActorRef = {
    keyActorRefMap get key match {
      case Some(ar) => ar
      case None =>  {
        val newRef: ActorRef = context.actorOf(props, name)
        //newRef shouldn't be present in the map already (if the key is different)
        actorRefKeyMap get newRef match{
          case Some(x) => throw new Exception{}
          case None =>
        }
        keyActorRefMap += Tuple2(key, newRef)
        actorRefKeyMap += Tuple2(newRef, key)
        newRef
      }
    }
  }

  def getOrCreateActorSimple(key: K, props: => Props): ActorRef = getOrCreateActor(key, props, key.toString)

  /**
   * method analogous to Actor's receive. Any subclasses should implement this method to handle all messages
   * except for the Terminate(ref) message passed from children
   */
  def responsibleReceive: Receive

  def receive: Receive = {
    case Terminated(ref) => {
      //removing both key and actor ref from both maps
      val pr: Option[Tuple2[K, ActorRef]] = for{
        key <- actorRefKeyMap.get(ref)
        reref <- keyActorRefMap.get(key)
      } yield (key, reref)

      pr match {
        case None => //error
        case Some((key, reref)) => {
          actorRefKeyMap -= ref
          keyActorRefMap -= key
        }
      }
    }
    case sth => responsibleReceive(sth)
  }
}

To use this functionality you inherit from ResponsibleActor and implement responsibleReceive. Note: this code isn't yet thoroughly tested and might still have some issues. I ommited some error handling to improve readability.



回答4:

Currently you can use Guice dependency injection with Akka, which is explained at http://www.lightbend.com/activator/template/activator-akka-scala-guice. You have to create an accompanying module for the actor. In its configure method you then need to create a named binding to the actor class and some properties. The properties could come from a configuration where, for example, a router is configured for the actor. You can also put the router configuration in there programmatically. Anywhere you need a reference to the actor you inject it with @Named("actorname"). The configured router will create an actor instance when needed.



标签: scala akka