Open Credo

January 18, 2016 | Software Consultancy

Akka Typed brings type safety to Akka framework

Last time in this series I summarised all the Akka Persistence related improvements in Akka 2.4. Since then Akka 2.4.1 has been released with some additional bug fixes and improvements so perhaps now is a perfect time to pick up this mini-series and introduce some other new features included in Akka 2.4.x.

WRITTEN BY

Rafal Gancarz

Rafal Gancarz

Akka Typed brings type safety to Akka framework

Previously in this series

I figured it’s about time to wrap up some unfinished blogging business, certainly before Akka team releases Akka 2.5. I’ve been rather busy and distracted lately to properly follow up on some of my previous posts (this is what happens when you are notoriously ambitious about how many blog posts you can produce while still working on client projects full time). Last time in this series I summarised all the Akka Persistence related improvements in Akka 2.4. Since then Akka 2.4.1 has been released with some additional bug fixes and improvements so perhaps now is a perfect time to pick up this mini-series and introduce some other new features included in Akka 2.4.x. Actually, initially I wanted to cover all remaining changes included in Akka 2.4 but after digging into Akka Typed it became apparent this single module deserves a dedicated blog post so here we are.

akka

Akka Typed

The next “new” addition to Akka 2.4 I wanted to introduce is Akka Typed. I say “new” because this experimental module is not the first attempt at creating a type-safe API for Akka. Previous attempts include Typed Actors and Typed Channels and the authors of Akka Typed module are quite open about the years of experimentation (not always successful) that has culminated with the module as can be found in Akka 2.4. Equally, the expectation is that it may take some more time for the type-safe API to mature up enough to replace the current one, centred around Actor trait.

Why the need for type safety in Akka API?

But what is all the fuss about? Akka toolkit has been around for some time now and seems to be doing rather well.

For anybody starting their journey with Akka it may not be the first thing on their mind to be bothered why every message that Actor can receive is untyped (instance of Any). Perhaps at first it may seem like a rather convenient way to avoid worrying about types but just get on with sending and receiving messages. Unfortunately, the current API suffers from a few drawbacks, which are to a good degree associated with the lack of type safety:

  • you can send any message to any actor and compiler won’t complain
  • because of all messages being untyped it relies on pattern matching to implement Actor.receive method which can lead to some messages not being handled and the compiler can’t assist here
  • it’s possible to close over unstable references from other execution contexts (for instance when mapping the Future) – calling sender() method from another execution context is a common beginner’s mistake
  • testing actor behaviour requires running it (behaviour is not decoupled from the execution/runtime) – using Akka TestKit helps but it’s really a workaround rather than a feature

Looking back at the Actor Model

A believe the other major reason to create a better API for Akka comes from the fact that the current API is not reflecting the Actor Model’s semantics as closely as it could. The most basic and succinct definition of an actor defines 3 actions that the actor can take in response to an incoming message:

  • send a finite number of messages to other actors;
  • create a finite number of new actors;
  • designate the behaviour to be used for the next message it receives.

One might argue that all the above rules are codified into the Actor trait (like become() and unbecome() for changing behaviours) but perhaps not as explicitly or neatly as it’s achieved in the API proposed in Akka Typed module and certainly not in a type-safe manner.

Show me the code!

By now you might be wondering how any of this translates to the actual code. If actors are not extending Actor trait how would you define and create an actor? If all the message types need to be statically declared, how can you create a non-trivial message protocol that multiple actors can use while benefiting from type safety? Lastly, how can actor behaviours change in response to incoming messages without stacking them up with become() and unbecome() (actually, the stack approach has been always rather problematic and rolling back to previous behaviour could easily lead to unexpected outcome).

I reckon the example used in the Akka Typed documentation illustrates how the typed API would be used in a more realistic situation. Consider we are building a chat application where multiple users can join a chat room and broadcast messages to all chat room members. Both the chat room and a chat client can be modelled as actors. To make the application more interesting and realistic let’s assume that each user maintains a session with the server.

Below is the snippet containing the protocol used for communication between the chat room actor and the chat client actors.

import akka.typed._
import akka.typed.ScalaDSL._
import akka.typed.AskPattern._
import scala.concurrent.Future
import scala.concurrent.duration._
import scala.concurrent.Await

sealed trait Command
final case class GetSession(screenName: String, replyTo: ActorRef[SessionEvent])
  extends Command
 
sealed trait SessionEvent
final case class SessionGranted(handle: ActorRef[PostMessage]) extends SessionEvent
final case class SessionDenied(reason: String) extends SessionEvent
final case class MessagePosted(screenName: String, message: String) extends SessionEvent
 
final case class PostMessage(message: String)

Type-safe message protocols with Akka Typed

Now, the first thing you might pick up from the code so far is how the entire message protocol is quite structured and strongly typed (rather than a random set of case classes/objects that you might be used to). Using sealed traits will make the compiler complain if you forget to handle any of the message types the behaviour of your actor should respond to (if you use the right type of behaviour, but more about it later).

The second difference compared with the standard Akka API that you might spot is the fact that ActorRef is parameterised with the type of the message you can send to the actor which address it represents. Additionally, because in Akka Typed there is no sender() reference available (prevents closing over unstable references) all actor addresses need to be made explicit in the message protocol – addressing has been brought into the domain of communication protocol where it belongs.

Actor’s behaviour as a first class citizen

Perhaps the most significant change in the new API is around how actor behaviours are expressed and the fact that now the behaviour is modelled separately from the actor itself and passed in at creation time. The example below shows how actor behaviour could be defined for the chat room actor.

private final case class PostSessionMessage(screenName: String, message: String)
  extends Command
 
val behavior: Behavior[GetSession] =
  ContextAware[Command] { ctx ⇒
    var sessions = List.empty[ActorRef[SessionEvent]]
 
    Static {
      case GetSession(screenName, client) ⇒
        sessions ::= client
        val wrapper = ctx.spawnAdapter {
          p: PostMessage ⇒ PostSessionMessage(screenName, p.message)
        }
        client ! SessionGranted(wrapper)
      case PostSessionMessage(screenName, message) ⇒
        val mp = MessagePosted(screenName, message)
        sessions foreach (_ ! mp)
    }
  }.narrow

A little cryptic at first perhaps, a Scala DSL provided by the Akka Typed module allows to define and compose static and dynamic (aka stateful actors) behaviours. Example above uses Static behaviour type and ContextAware behaviour decorator to deliver the functionality what previously would have been available in Actor trait (good example you can fix any problem by introducing a layer of indirection). One additional thing to note in the snippet above is ActorContext.spawnAdapter() method used to create a child actor that translates different message types and hence help creating a rich and type-safe message protocol.

Modelling the dynamic behaviour requires either a Partial or a Total behaviour type that takes a function (partial or total respectively) mapping incoming typed message to the new behaviour, including a few predefined behaviours provided by the DSL like Same (behaviour doesn’t change) or Stopped (results in an actor to be terminated). The snippet below implements a possible behaviour of the chat client.

import ChatRoom._
 
val gabbler: Behavior[SessionEvent] =
  Total {
    case SessionDenied(reason) ⇒
      println(s"cannot start chat room session: $reason")
      Stopped
    case SessionGranted(handle) ⇒
      handle ! PostMessage("Hello World!")
      Same
    case MessagePosted(screenName, message) ⇒
      println(s"message has been posted by '$screenName': $message")
      Stopped
  }

Lifecycle events as messages

Akka Typed embraces communication via passing messages and lifts lifecycle hooks previously being part of the Actor trait to be handled as typed messages. DSL offers two dedicated behaviour types called Full (taking a partial function) and FullTotal (taking a total function) that include lifecycle messages among those exposed to the behaviour they compose. Lifecycle event specific messages are wrapped in a Sig case class, whereas normal messages are wrapped in Msg case class. Below you can see the behaviour of the main actor of the chat application responding to some lifecycle event messages.

val main: Behavior[Unit] =
  Full {
    case Sig(ctx, PreStart) ⇒
      val chatRoom = ctx.spawn(Props(ChatRoom.behavior), "chatroom")
      val gabblerRef = ctx.spawn(Props(gabbler), "gabbler")
      ctx.watch(gabblerRef)
      chatRoom ! GetSession("ol’ Gabbler", gabblerRef)
      Same
    case Sig(_, Terminated(ref)) ⇒
      Stopped
  }
 
val system = ActorSystem("ChatRoomDemo", Props(main))
Await.result(system.whenTerminated, 1.second)

Some thoughts on the Akka Typed API/DSL

As I was getting deeper into the codebase of Akka Typed I could not stop wondering about some of the design choices behind the API and accompanying DSL. I definitely agree with the general direction of the project but do have a few comments on the proposed API:

  • Based on the actions any actor can take I believe that creating child actors should be available without having to extract ActorContext using ActorContextAware decorator (same might be argued for actor’s own address, especially that now it needs to passed around explicitly). In general, I suspect there is a lot of work required to cleanly separate actors behaviour (including state management) and actor’s runtime or more transient properties (currently encompassed in ActorContext).
  • Judging by different behaviour types it strucks me like there should be a better way to model behaviour transitions in combination with handling lifecycle events as messages. The existence of Full vs FullTotal as well as MessageOrSignal trait indicate more work is needed to separate different concerns at the API level.
  • I was wondering how the new API could coexist (or ideally replace) FSM (Finite State Machine) actors – ideally the new API should be expressive enough to provide FSM capabilities, although I could be asking for too much here.
  • I’ve been also thinking about persistence. With Actor and by extension PersistentActor traits gone, persistence concern needs to be incorporated into or supported by the typed API, ideally in a non-intrusive way.

Summary

Akka Typed is the best to date attempt to bring type safety to Akka framework and certainly looks very promising. Key design decisions like separating actor’s behaviour into a separate concern as well as treating lifecycle events as messages will result in a less coupled and much safer programming model. The work is still very much in progress and I hope we will see further changes in upcoming Akka releases, eventually leading to a completely revamped API for Akka that will make using the framework easier and less error-prone by leveraging Scala’s type system. API/DSL development is one of the most satisfying (and challenging) areas in programming and I strongly encourage all you hakkers to have a stab at playing with Akka Typed and trying to improve it (you can start with any of the issues listed in Github).

Next time

I haven’t anticipated to spend as much time looking at Akka Typed but the fascination with API/DSL development got the better of me. There are two more areas I want to cover among many changes included in Akka 2.4: new Distributed Data module and ongoing improvements to Akka Cluster. Stay tuned.

RETURN TO BLOG

SHARE

Twitter LinkedIn Facebook Email

SIMILAR POSTS

Blog