Search code examples
scalaakkatypesafe

Akka actor inheritance with context.become


I have a tricky issue when trying to change a child actor's state with the become method. I implemented actor inheritance as per the Receiving trait suggested here:

trait Mcma extends Receiving with ActorLogging {
  val actorName: String

  /**
    * Simple method to be overridden when necessary
    * @param entity the response entity
    */
  protected def handleHttpOK(entity: ResponseEntity): Unit = log.info(s"$actorName got unhandled OK response")

  // Add http responses match if necessary
  receiver {
    case HttpResponse(StatusCodes.OK, _, entity, _) => handleHttpOK(entity)

    case resp @ HttpResponse(code, _, _, _) =>
      log.error(s"$actorName got response code: {}", code)
      // Discard the flow to avoid backpressure
      resp.discardEntityBytes()

    case e: Status.Failure => log.error(s"$actorName got failure: {}", e.cause.getMessage)

    case _ => log.warning(s"Unexpected message in $actorName")
  }
}

The problem occurs when I have an actor that implements this Mcma trait and changes its own state with the become pattern:

class Reseau(url: String, optSender: Option[ActorRef]) extends Mcma with Receiving {
  override val actorName: String = "ReseauActor"

  /**
    * The active method used to handle actor state change with the become helper
    * @param queryDataStr the query string for each call to ReseauActor
    * @return
    */
  def active(queryDataStr: String): Receive = {
    case s: String => context become active(s)
  }

  // Init the actor with empty query
  receiver(
    active("")
  )
}

Once the context has changed, the default inherited matching cases like e or _ are not matched anymore. There is most likely an obvious issue...

[Updated working version with answer from Evgeny] With the mentioned Receiving trait, Reseau actor becomes

class Reseau(url: String, optSender: Option[ActorRef]) extends Mcma {
  override val actorName: String = "ReseauActor"

  override def preStart(): Unit = {
    super.preStart()
    // Init the actor with empty query
    context become receiver(active(""))
  }

  /**
    * The active method used to handle actor state change with the become helper
    *
    * @param queryDataStr the query string for each call to ReseauActor
    * @return
    */
  def active(queryDataStr: String): Receive = {

    case s: String => context become receiver(active(s))
  }
}

and the Mcma trait:

trait Mcma extends Receiving with ActorLogging {
  val actorName: String

  /**
    * Simple method to be overridden when necessary
    * @param entity the response entity
    */
  protected def handleHttpOK(entity: ResponseEntity): Unit = log.info(s"$actorName got unhandled OK response")

  // For http response handling
  final implicit val materializer: ActorMaterializer = ActorMaterializer(ActorMaterializerSettings(context.system))

  override def preStart(): Unit = log.info(s"$actorName started")

  override def postStop(): Unit = log.info(s"$actorName stopped")

  // Add http responses match if necessary
  addReceiver {
    case HttpResponse(StatusCodes.OK, _, entity, _) => handleHttpOK(entity)

    case resp @ HttpResponse(code, _, _, _) =>
      log.error(s"$actorName got response code: {}", code)
      // Discard the flow to avoid backpressure
      resp.discardEntityBytes()

    case e: Status.Failure => log.error(s"$actorName got failure: {}", e.cause.getMessage)

    case _ => log.warning(s"Unexpected message in $actorName")
  }
}

and in order to define an actor that would not need to change his internal state, you would just do:

class TLRVJob1() extends Mcma {
  override val actorName: String = "TLRVJob1Actor"

  addReceiver {
    case t: Get => ???
  }
}

Solution

  • I would suggest to modify initial Receiving trait:

    trait Receiving extends Actor {
      var receivers: Receive = Actor.emptyBehavior
      def addReceiver(next: Actor.Receive): Unit = {receivers = receiver(next)}
      def receiver(next: Actor.Receive): Receive = {receivers orElse next}
    
      def receive: Receive = receivers
    }
    

    Now, you have two cases, to add permanent part of receive and have class-dependent part for become. Change receiver with addReceiver in hierarchy, and init your Reseau class: instead of

    receiver(
      active("")
    )
    

    move this into preStart in form:

    override def preStart(): Unit = {
      super.preStart()
      context become receiver(active(""))
    }
    

    and change active method body to

    context become receiver(active(s))