Creating stateless actors by changing their behaviour

Reading Time: 3 minutes

Hey folks, let us understand how we can create stateless actors by changing their behaviour. I hope you have a basic understanding of what akka actors are and how do they work. If not, then you can check out this blog.

Introduction

An actor is described by its behaviour, which is responsible for handling the messages that the actor can receive. After each message, the actor’s behaviour can change: given new information, the actor might change the way it handles future messages – much like us humans in real life.

An important part of an actor is the potential data it might hold – we call that its state. As a reaction to an incoming message, the data held inside the actor might change.

Stateful actor

Let us now see how a stateful actor looks like.

object StatefulBankAccount {
  case class Withdraw(amount: Int)
  case class Deposit(amount: Int)
  case object ShowBalance
}
class StatefulBankAccount extends Actor with ActorLogging{
  import StatefulBankAccount._

  var balance = 0
  override def receive: Receive = {
  case Withdraw(amount) =>
    if(balance < amount) log.info("Insufficient balance")
    else {
      log.info(s"$amount withdrawn")
      balance = balance - amount
    }
  case Deposit(amount) =>
    log.info(s"$amount deposited")
      balance = balance + amount
  case ShowBalance => log.info(s"Total balance: $balance")
  }
}
import StatefulBankAccount._

val system = ActorSystem("changingActorBehaviorDemo")
val bankAccount = system.actorOf(Props[StatefulBankAccount],"BankAccount")
bankAccount ! Deposit(2000)
bankAccount ! Withdraw(2500)
bankAccount ! Withdraw(500)
bankAccount ! ShowBalance

The output of the above code would be:

INFO] [dd/mm/yyyy hh:mm:ss] [changingActorBehaviorDemo-akka.actor.default-dispatcher-3] [akka://changingActorBehaviorDemo/user/BankAccount] 2000 deposited
[INFO] [dd/mm/yyyy hh:mm:ss] [changingActorBehaviorDemo-akka.actor.default-dispatcher-3] [akka://changingActorBehaviorDemo/user/BankAccount] Insufficient balance
[INFO] [dd/mm/yyyy hh:mm:ss] [changingActorBehaviorDemo-akka.actor.default-dispatcher-3] [akka://changingActorBehaviorDemo/user/BankAccount] 500 withdrawn
[INFO] [dd/mm/yyyy hh:mm:ss] [changingActorBehaviorDemo-akka.actor.default-dispatcher-3] [akka://changingActorBehaviorDemo/user/BankAccount] Total balance: 1500

There are 2 big issues with the stateful actors:

  1. The messages are processed by the actor on the basis of the current state of an actor. The state in the above case is a simple variable, but it might be very complex in big applications and it would become very difficult to write logic for handling messages in that case.
  2. We should avoid mutability in our code and using stateful actors leads to mutability.

Stateless actor

Now, let us see how we can create a stateless actor from the above code.

object StatelessBankAccount {
  case class Withdraw(amount: Int)
  case class Deposit(amount: Int)
  case object ShowBalance
}
class StatelessBankAccount extends Actor with ActorLogging{
  import StatelessBankAccount._

  override def receive: Receive = bankAccountReceive(0)

  def bankAccountReceive(balance: Int): Receive = {
    case Withdraw(amount) =>
      if(balance < amount) log.info("Insufficient balance")
      else {
        log.info(s"$amount withdrawn")
        context.become(bankAccountReceive(balance - amount))
      }
    case Deposit(amount) =>
      log.info(s"$amount deposited")
      context.become(bankAccountReceive(balance + amount))
    case ShowBalance => log.info(s"Total balance: $balance")
  }
}
import StatelessBankAccount._

val system = ActorSystem("changingActorBehaviorDemo")
val bankAccount = system.actorOf(Props[StatelessBankAccount],"BankAccount")
bankAccount ! Deposit(2000)
bankAccount ! Withdraw(2500)
bankAccount ! Withdraw(500)
bankAccount ! ShowBalance

The output of the above code would be same as in case of stateful actors.

So, for stateless actors, instead of using a mutable variable, we use context.become() and provide a message handler to this method which in this case would be the same message handler.

Reverting to previous behaviour

context.become() method takes 2 parameters – first parameter is the message handler and second parameter is the boolean value. Whenever we call context.become(message-handler), it by default takes true as its second parameter. Passing true as it’s second parameter means that it will discard the previous message handler and replaces it with the new message handler. Passing false as it’s second parameter means that it will stack the new message handler onto the old message handler.

We can use context.unbecome() to remove the handler from top of the stack and it goes back to its previous handler which is on the top of the stack. In case the stack is empty, compiler will call the default message handler. We use this concept of reverting the behaviour only when there are at least 2 kind of message handlers.

Let us look at this through some code.

object Wrestler {
  case object JunkFood
  case object HealthyFood
  case object status
}

class Wrestler extends Actor with ActorLogging {
  import Wrestler._

  override def receive: Receive = strongReceive

  def strongReceive: Receive = {
    case JunkFood => context.become(weakReceive, false)
    case HealthyFood =>
    case status => log.info("Wrestler is strong")
  }

  def weakReceive: Receive = {
    case JunkFood => context.become(weakReceive, false)
    case HealthyFood => context.unbecome()
    case status => log.info("Wrestler is weak")
  }
}
import Wrestler._

val system = ActorSystem("changingActorBehaviorDemo")
val wrestler = system.actorOf(Props[Wrestler])
wrestler ! JunkFood
wrestler ! JunkFood
wrestler ! HealthyFood
wrestler ! status

The output of the above code would be:

[INFO] [dd/mm/yyyy hh:mm:ss] [changingActorBehaviorDemo-akka.actor.default-dispatcher-2] [akka://changingActorBehaviorDemo/user/$a] Wrestler is weak

In the above code, strongReceive handler will be pushed into stack because strongReceive handler is passed to overridden receive handler. After sending JunkFood message 2 times the stack would look like this:

  • weakReceive
  • weakReceive
  • strongReceive

On sending HealthyFood message, weakReceive handler from top of the stack will be removed and the second weakReceive handler will be on the top of stack. Finally, on sending status message, as weakReceive handler is set, “Wrestler is weak” will be logged.

So I hope you understood what are the drawbacks of stateful actors and how we can convert a stateful actor into a stateless actor. Also you might have understood about how we can revert back to our previous message handler by using context.unbecome().

Written by 

Bhavya Verma is a Software Consultant at Knoldus. An avid Scala programmer, he is recognised as a good team player, dedicated and responsible professional, and a technology enthusiast. He has good time management skills. His hobbies include playing badminton, watching movies and travelling.

Discover more from Knoldus Blogs

Subscribe now to keep reading and get access to the full archive.

Continue reading