Initializing an actor before being able to handle

2020-03-18 03:32发布

问题:

I have an actor which creates another one:

class MyActor1 extends Actor {
  val a2 = system actorOf Props(new MyActor(123))
}

The second actor must initialize (bootstrap) itself once it created and only after that it must be able to do other job.

class MyActor2(a: Int) extends Actor {
  //initialized (bootstrapped) itself, potentially a long operation 
  //how?
  val initValue = // get from a server

  //handle incoming messages
  def receive = {
    case "job1" => // do some job but after it's initialized (bootstrapped) itself
  }
}

So the very first thing MyActor2 must do is do some job of initializing itself. It might take some time because it's request to a server. Only after it finishes successfully, it must become able to handle incoming messages through receive. Before that - it must not do that.

Of course, a request to a server must be asynchronous (preferably, using Future, not async, await or other high level stuff like AsyncHttpClient). I know how to use Future, it's not a problem, though.

How do I ensure that?

p.s. My guess is that it must send a message to itself first.

回答1:

You could use become method to change actor's behavior after initialization:

class MyActor2(a: Int) extends Actor {

  server ! GetInitializationData

  def initialize(d: InitializationData) = ???

  //handle incoming messages
  val initialized: Receive = {
    case "job1" => // do some job but after it's initialized (bootstrapped) itself
  }

  def receive = {
    case d @ InitializationData =>
      initialize(d)
      context become initialized
  }
}

Note that such actor will drop all messages before initialization. You'll have to preserve these messages manually, for instance using Stash:

class MyActor2(a: Int) extends Actor with Stash {

  ...

  def receive = {
    case d @ InitializationData =>
      initialize(d)
      unstashAll()
      context become initialized
    case _ => stash()
  }
}

If you don't want to use var for initialization you could create initialized behavior using InitializationData like this:

class MyActor2(a: Int) extends Actor {

  server ! GetInitializationData

  //handle incoming messages
  def initialized(intValue: Int, strValue: String): Receive = {
    case "job1" => // use `intValue` and `strValue` here
  }

  def receive = {
    case InitializationData(intValue, strValue) =>
      context become initialized(intValue, strValue)
  }
}


回答2:

I don't know wether the proposed solution is a good idea. It seems awkward to me to send a Initialization message. Actors have a lifecycle and offer some hooks. When you have a look at the API, you will discover the prestart hook.

Therefore i propose the following:

  • When the actor is created, its preStart hook is run, where you do your server request which returns a future.
  • While the future is not completed all incoming messages are stashed.
  • When the future completes it uses context.become to use your real/normal receive method.
  • After the become you unstash everything.

Here is a rough sketch of the code (bad solution, see real solution below):

class MyActor2(a: Int) extends Actor with Stash{

  def preStart = {
    val future = // do your necessary server request (should return a future)
    future onSuccess {
      context.become(normalReceive)
      unstash()
    }
  }

  def receive = initialReceive

  def initialReceive = {
    case _ => stash()
  }

  def normalReceive = {
    // your normal Receive Logic
  }
}

UPDATE: Improved solution according to Senias feedback

class MyActor2(a: Int) extends Actor with Stash{

  def preStart = {
    val future = // do your necessary server request (should return a future)
    future onSuccess {
      self ! InitializationDone
    }
  }

  def receive = initialReceive

  def initialReceive = {
    case InitializationDone =>
      context.become(normalReceive)
      unstash()
    case _ => stash()
  }

  def normalReceive = {
    // your normal Receive Logic
  }

  case class InitializationDone
}