SergiGP
SergiGP

Reputation: 691

Get or create child actor by ID

I have two actors in my system. Talker and Conversation. Conversation consists in two talkers (by now). When a Talker wants to join a conversation I should check if conversation exists (another talker has created it) and if it not, create it. I have this code in a method of my Talker actor:

  def getOrCreateConversation(conversationId: UUID): ActorRef = {

    // @TODO try to get conversation actor by conversationId
    context.actorSelection("user/conversation/" + conversationId.toString)

    // @TODO if it not exists... create it
    context.actorOf(Conversation.props(conversationId), conversationId.toString)
  }

As you can see, when I create my converastion actor with actorOf I'm passing as a second argument the conversationId. I do this for easy searching this actor... Is it the correct way to do this?

Thank you

edited

Thanks to @Arne I've finally did this:

class ConversationRouter extends Actor with ActorLogging {
  def receive = {
    case ConversationEnv(conversationId, msg) =>
      val conversation = findConversation(conversationId) match {
        case None    => createNewConversation(conversationId)
        case Some(x) => x
      }
      conversation forward msg
  }

  def findConversation(conversationId: UUID): Option[ActorRef] = context.child(conversationId.toString)

  def createNewConversation(conversationId: UUID): ActorRef = {
    context.actorOf(Conversation.props(conversationId), conversationId.toString)
  }
}

And the test:

class ConversationRouterSpec extends ChatUnitTestCase("ConversationRouterSpec") {

  trait ConversationRouterSpecHelper {
    val conversationId = UUID.randomUUID()

    var newConversationCreated = false

    def conversationRouterWithConversation(existingConversation: Option[ActorRef]) = {
      val conversationRouterRef = TestActorRef(new ConversationRouter {
        override def findConversation(conversationId: UUID) = existingConversation

        override def createNewConversation(conversationId: UUID) = {
          newConversationCreated = true
          TestProbe().ref
        }
      })
      conversationRouterRef
    }
  }

  "ConversationRouter" should {
    "create a new conversation when a talker join it" in new ConversationRouterSpecHelper {
      val nonExistingConversationOption = None
      val conversationRouterRef = conversationRouterWithConversation(nonExistingConversationOption)

      conversationRouterRef ! ConversationEnv(conversationId, Join(conversationId))

      newConversationCreated should be(right = true)
    }

    "not create a new conversation if it already exists" in new ConversationRouterSpecHelper {
      val existingConversation = Option(TestProbe().ref)
      val conversationRouterRef = conversationRouterWithConversation(existingConversation)

      conversationRouterRef ! ConversationEnv(conversationId, Join(conversationId))

      newConversationCreated should be(right = false)
    }
  }
}

Upvotes: 2

Views: 892

Answers (1)

Arne Claassen
Arne Claassen

Reputation: 14414

Determining the existence of an actor cannot be done synchronously. So you have a couple of choices. The first two are more conceptual in nature to illustrate doing asynchronous lookups, but I offer them more for reference about the asynchronous nature of actors. The third is likely the correct way of doing things:

1. Make the function return a Future[ActorRef]

def getOrCreateConversation(conversationId: UUID): Unit {
   context.actorSelection(s"user/conversation/$conversationId")
     .resolveOne()
     .recover { case _:Exception =>
        context.actorOf(Conversation.props(conversationId),conversationId.toString)
      }
}

2. Make it Unit and have it send the ActorRef back to your current actor

Pretty much the same as the above, but now you we pipe the future back the current actor, so that the resolved actor can be dealt with in the context of the calling actor's receive loop:

def getOrCreateConversation(conversationId: UUID): Unit {
   context.actorSelection(s"user/conversation/$conversationId")
     .resolveOne()
     .recover { case _:Exception =>
        context.actorOf(Conversation.props(conversationId),conversationId.toString)
      }.pipeTo(self)
}

3. Create a router actor that you send your Id'ed messages to and it creates/resolves the child and forwards the message

I say that this is likely the correct way, since your goal seems to be cheap lookup at a specific named path. The example you give makes the assumption that the function is always called from within the actor at path /user/conversation otherwise the context.actorOf would not create the child at /user/conversation/{id}/.

Which is to say that you have a router pattern on your hands and the child you create is already known to the router in its child collection. This pattern assumes you have an envelope around any conversation message, something like this:

case class ConversationEnv(id: UUID, msg: Any)

Now all conversation messages get sent to the router instead of to the conversation child directly. The router can now look up the child in its child collection:

def receive = {
  case ConversationEnv(id,msg) =>
    val conversation = context.child(id.toString) match {
      case None => context.actorOf(Conversation.props(id),id.toString)
      case Some(x) => x
    }
    conversation forward msg
}

The additional benefit is that your router is also the conversation supervisor, so if the conversation child dies, it can deal with it. Not exposing the child ActorRef to the outside world also has the benefit that you could have it die when idle and have it get re-created on the next message receipt, etc.

Upvotes: 8

Related Questions