Scala

Akka Typed: First steps with typed actors in Scala

With the [release of Akka 2.4.0| http://akka.io/news/] a couple of weeks ago the experimental Akka Typed module was added. With Akka Typed it is possible to create and interact with Actors in a type safe manner. So instead of just sending messages to an untyped Actor, with Akka Typed we can add compile time type checking to our Actor interactions. Which of course is a great thing! In this first article on Akka Typed we’ll look at a couple of new concepts of Akka Typed and how you can create and communicate with Actors in this new way. The complete code used in this article can be found in this Gist.

As always lets quickly show the SBT build file:

name := "akka-typed"
 
version := "1.0"
 
scalaVersion := "2.11.7"
 
libraryDependencies += "com.typesafe.akka" %% "akka-typed-experimental" % "2.4.0"

This wil pull in the required main Akka libraries and the new Akka Typed way of creating Actors. Once added we can create our first actor. One of the biggest differences is that we don’t explicitly create Actors anymore, but we define Behavior and from that behaviour we create an actor. To make it easy for you, Akka Typed comes with a lot of helper classes to make defining Behavior easier. In this article we’ll introduce a number of these.

Simple static Behavior

First lets look at the code for a simple static actor. Note that we show some additional case classes to make the examples a little bit more useful:

import scala.concurrent.ExecutionContext.Implicits.global
  implicit val timeout = Timeout(5 seconds)

  // 1. First simple example, we'll create a typed actor
  // which just prints out the received message.
  println("Step 1: Using static Actor")

  // which can only receive HelloMsg message.
  sealed trait HelloMsg
  final case class HelloCountry(country: String) extends HelloMsg
  final case class HelloCity(city: String) extends HelloMsg
  final case class HelloWorld() extends HelloMsg
  final case class Hello(msg: String) extends HelloMsg

  // simple static actor, which just prints out the message
  val helloSayer = Static[HelloMsg] { msg =>
    println("Msg received:" + msg);
  }

  // create a new root actor and send it types messages
  val helloSystem: ActorSystem[HelloMsg] = ActorSystem("helloSayer", Props(helloSayer))
  helloSystem ! HelloCountry("Netherlands")
  helloSystem ! HelloWorld()
  helloSystem ! HelloCity("Waalwijk")
  helloSystem ! Hello("HelloHelloHello")

We first define some simple implicits and a set of case classes which we’ll send to our Actor. The actual Behavior is defined using the Static case class. A Static Behavior, as the name implies, provides a non-changing actor which always executes in the same manner. In this case we just print out the message we received. To create an actor from this Behavior , we initiate a new ActorSystem. This ActorSystem can be seen as the root actor and we can send messages to it. Sending messages happens in the same manner, and the result from this piece of code is the following:

Step 1: Using static Actor
Msg received:HelloCountry(Netherlands)
Msg received:HelloWorld()
Msg received:HelloCity(Waalwijk)
Msg received:Hello(HelloHelloHello)

Note that with a Static actor we don’t handle any lifecycle signals, we just process the incoming message, and handle it in the same manner for each and every request.

Using the ask pattern

Another important pattern of Akka actors is the ability to use the ask pattern. With the ask pattern we send a message to an actor and get a Future[T] that contains the response. In Akka Typed you use the following approach for this:

// 2. Now we use an actor that responds to the sending actor using the
  // ask pattern.
  Thread.sleep(1000)
  println("\n\nStep 2: Using reply Actor")

  // Simple case class, which is used for the ask pattern
  final case class HelloReply(say: String, replyTo: ActorRef[HelloMsg])

  // static actor that responds to the passed in actorRef
  val helloReplyer = Static[HelloReply] {msg =>
    msg.replyTo ! Hello(s"You said: ${msg.say} ")
  }

  // create a new system and use the ask pattern to send it messages.
  val replySystem: ActorSystem[HelloReply] = ActorSystem("hello", Props(helloReplyer))
  val response = replySystem ? { f:ActorRef[HelloMsg] => HelloReply("Hello", f)}
  // or use a shorter form: val response = replySystem ? (HelloReply("Hello", _))
  val res = Await.result(response, 5 seconds)
  println("Response recevied: " + res)

For this message we use an additional case class. This case class not just contains the message, but also an actorRef to which to respond. The reason this is done, is because in Akka Typed we can’t access the sender directly in the actor, so we need to pass it in, in the incoming message. Our actor Behavior is very straightforward. We just send a new message back to the passed in actor through its actorRef. The interesting thing here, that the request as well as the response are typed. To ask something of an actor we use the familiar ‘?’ operation, and since we added a replyTo field to our case class, we pass in the anonymous actor that is created when we use the ? function. The ? operation returns a Future[HelloMsg], on which we just wait in this example.

When we run this example we get the following:

Step 2: Using reply Actor
Response recevied: Hello(You said: Hello )

Switching out actor behavior

One of the cool things about actors is that they can switch behavior based on processed messages. This works very nice for implementing state machines, protocol handlers etc. With Typed Actors we can of course also accomplish this. Lets first look at the code for this example:

// two functions which we'll switch in the actor implementation. One
  // prints everything in lower case, the ohter in uppercase
  val f1 = (msg: HelloMsg) => {println(s"In total function: $msg".toLowerCase)}
  val f2 = (msg: HelloMsg) => {println(s"In total function: $msg".toUpperCase)}

  // create a new Total 'Actor'. It runs the first function and returns the second one
  // effectively switching the implementation between the two functions. Note that we use
  // the Total behavior for this example. With a Total we don't handle any system messages
  // of type [Signal], if we want to do that we could use the FullTotal instead
  def newTotal(f1: HelloMsg => Unit, f2: HelloMsg => Unit): Total[HelloMsg] = Total[HelloMsg] { msg =>
    f1(msg)
    newTotal(f2, f1)
  }

  val behavior1: Behavior[HelloMsg] = newTotal(f1, f2) // normally we can send all the base traits
  val behavior2: Behavior[HelloWorld] = newTotal(f1, f2).narrow // by using narrow we can limit the behavior to a type

  // now create a new actor, and use the function to create the stateless total
  val totalSystem: ActorSystem[HelloMsg] = ActorSystem("hello", Props(behavior1))
  totalSystem ! HelloCountry("Netherlands")
  totalSystem ! HelloWorld()
  totalSystem ! HelloCity("Waalwijk")
  totalSystem ! Hello("HelloHelloHello")

In this code fragment we use the Total[T] case class to implement the switching behavior. With a Total case class we define the behavior that needs to be executed, when a message is processed. Besides that we also need to return the new Behavior that will execute when the next message is received. In this example we switch two different behaviors. The first one prints out everything in uppercase, and the other one in lowercase. So the first message will be printed in complete lowercase, the second in uppercase and so on.

This results in the following output:

in total function: hellocountry(netherlands)
IN TOTAL FUNCTION: HELLOWORLD()
in total function: hellocity(waalwijk)
IN TOTAL FUNCTION: HELLO(HELLOHELLOHELLO)

Using the Full behavior

So far we’ve only looked at how to process messages. Besides processing messages, some behaviours might also need to respond to lifecycle events. In the traditional way this would mean overriding specific lifecycle functions. With Akka Typed, however, we don’t have access to these functions anymore. Lifecycle events are delivered to a behavior in the same way as normal messages. If you need direct access to these, you can use a different way to construct your behavior. One of the options is to use the Full[T] class for this:

val fullBehavior = ContextAware[HelloMsg] { ctx =>
    println(s"We can access the context: $ctx")
    Full[HelloMsg] {
      case msg: MessageOrSignal[HelloMsg] => println(s"Recevied messageOrSignal: $msg"); Same[HelloMsg]
    }
  }

  val fullSystem: ActorSystem[HelloMsg] = ActorSystem("hello", Props(fullBehavior))
  fullSystem ! HelloCountry("Netherlands")
  fullSystem ! HelloWorld()
  fullSystem ! HelloCity("Waalwijk")
  fullSystem ! Hello("HelloHelloHello")

As you can see by using the Full[T] class we get the message or the signal in a MessageOrSignal envelop, which we can process in the same way as we would do normally. Note that we’ve also added a decorator around this actor. Using the ContextAware decorator we can make the actor context available to the behavior (we don’t use it here any further though).

The output from these message looks like:

We can access the context: akka.typed.ActorContextAdapter@ee559a3
Recevied messageOrSignal: Sig(akka.typed.ActorContextAdapter@ee559a3,PreStart)
Recevied messageOrSignal: Msg(akka.typed.ActorContextAdapter@ee559a3,HelloCountry(Netherlands))
Recevied messageOrSignal: Msg(akka.typed.ActorContextAdapter@ee559a3,HelloWorld())
Recevied messageOrSignal: Msg(akka.typed.ActorContextAdapter@ee559a3,HelloCity(Waalwijk))
Recevied messageOrSignal: Msg(akka.typed.ActorContextAdapter@ee559a3,Hello(HelloHelloHello))
 
// when the system is shutdown
Recevied messageOrSignal: Sig(akka.typed.ActorContextAdapter@ee559a3,PostStop)

As you can see we receive either a message or a signal.

Combining behaviors

For the last example in this article, we’ll have a quick look on how to combine behaviors together to create new ones. For this Akka Typed introduces the following two operators:

  • &&: Sends the message to both behaviours.
  • ||: First sends the message to the left behavior, if that behavior returns an unhandled response, the right side is tried.

In code this looks like this:

val andCombined = helloSayer && behavior1
  val andSystem: ActorSystem[HelloMsg] = ActorSystem("and",Props(andCombined))
  andSystem ! HelloCountry("Netherlands")
  andSystem ! HelloWorld()
  andSystem ! HelloCity("Waalwijk")
  andSystem ! Hello("HelloHelloHello")


  Thread.sleep(1000)
  println("\n\nStep 5a: Use the || combinator")
  // first try the left one, if it is unhandled tries the right one
  val orCombined = Total[HelloMsg]{
    case _ => {println("Can't handle it here!");Unhandled}
  } || fullBehavior
  val orSystem: ActorSystem[HelloMsg] = ActorSystem("or", Props(orCombined))
  orSystem ! HelloCountry("Netherlands")
  orSystem ! HelloWorld()
  orSystem ! HelloCity("Waalwijk")
  orSystem ! Hello("HelloHelloHello")

The && system is very straightforward, and we just reuse existing behaviors. For the || functionality we combine a new Behavior, which just always returns Unhanded, so should pass all messages to the fullBehavior behavior.

The result for the && operator looks like this:

Msg received:HelloCountry(Netherlands)
in total function: hellocountry(netherlands)
Msg received:HelloWorld()
IN TOTAL FUNCTION: HELLOWORLD()
Msg received:HelloCity(Waalwijk)
in total function: hellocity(waalwijk)
Msg received:Hello(HelloHelloHello)
IN TOTAL FUNCTION: HELLO(HELLOHELLOHELLO)

As you can see the message is processed by both behaviors. For the || operator the output looks like this:

We can access the context: akka.typed.ActorContextAdapter@4cba0952
Recevied messageOrSignal: Sig(akka.typed.ActorContextAdapter@4cba0952,PreStart)
Can't handle it here!
Recevied messageOrSignal: Msg(akka.typed.ActorContextAdapter@4cba0952,HelloCountry(Netherlands))
Can't handle it here!
Recevied messageOrSignal: Msg(akka.typed.ActorContextAdapter@4cba0952,HelloWorld())
Can't handle it here!
Recevied messageOrSignal: Msg(akka.typed.ActorContextAdapter@4cba0952,HelloCity(Waalwijk))
Can't handle it here!
Recevied messageOrSignal: Msg(akka.typed.ActorContextAdapter@4cba0952,Hello(HelloHelloHello))

Here, we can see that after a “Can’t handle it here!” message the right side of the operator takes over.

first conclusions

This was just a quick first look at Typed Actors. For me it felt like a very nice way of creating actor systems so far. It feels very intuitive and the fact that the requests and responses both can be types will most likely result in more secure code. In a future article we’ll get back to this subject.

Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button