A few posts ago I looked at Erlang and how it solves concurrency using the actor model. I truly enjoyed playing with this strategy and found the concept intriguing.
That's why i decided to look into how concurrency using actors is solved in other languages as well. In this post I'll take a quick look at Scala and how we can work with actors using the Akka library.
It's worth mentioning that Scala originally had it’s own Actor library. However, in version 2.10 Akka became the default Actor library and in 2.11 the Scala Actor library got deprecated.
So what are we creating?
I’m going to keep it to the absolute basic and recreate the example I made in the Erlang post.
We created a small chatroom where we had two types of actors, server actors and client actors. Clients will then be able to connect to the server in order to participate in the chatroom.
Communication between the actors
In Akka, actors are very lightweight concurrent entities that communicate asynchronously by sending messages between each other. As with Erlang, Scala uses pattern matching to figure out what kind of a messages it receives.
To create the different types of messages, we just define a few case classes.
abstract class Msg
case class Send(msg: String) extends Msg
case class NewMsg(from: String, msg: String) extends Msg
case class Info(msg: String) extends Msg
case class Connect(username: String) extends Msg
case class Broadcast(msg: String) extends Msg
case object Disconnect extends Msg
We'll see how the different case classes are used when we're studying the actor implementations. So let's get to it!
Time to look at the server actor
class Server extends Actor {
var clients = List[(String, ActorRef)]();
def receive = {
case Connect(username) => {
broadcast(Info(f"$username%s joined the chat"))
clients = (username,sender) :: clients
context.watch(sender)
}
case Broadcast(msg) => {
val username = getUsername(sender)
broadcast(NewMsg(username, msg))
}
case Terminated(client) => {
val username = getUsername(client)
clients = clients.filter(sender != _._2)
broadcast(Info(f"$username%s left the chat"))
}
}
def broadcast(msg: Msg) {
clients.foreach(x => x._2 ! msg)
}
def getUsername(actor: ActorRef): String = {
clients.filter(actor == _._2).head._1
}
}
To define an actor, we need to create a class that extends the Actor
trait. This trait contains several methods that we can override to add functionality. However, for this post we're going to focus on the receive
method.
receive
is the only abstract method that we need to override to create a valid actor. This method will fetch messages from the mailbox and run it through our pattern matching to figure out what to do.
Let's go through the different patterns
In the server actor we've defined three patterns. Let's take a look at what they do.
Connect(username)
- Cool, someone is trying to connect to our chat!
The first thing we do is to notify all the other clients that a new client has connected.
As with Erlang, we use the exclamation point to send messages to other actors.
Next we add the new client to our client list. sender
is defined in the Actor trait and contains the ActorRef
of the message sender.
The last thing we do is to make the server start monitoring the client actor by calling watch
. Doing this, the server actor will receive a Terminated
message if the client actor is terminated. Pretty cool right?
Broadcast(msg)
- A new message for the chatroom!
This case doesn't contain anything special. It just finds the username of the sender based on the ActorRef
and then sends a NewMsg
to all the clients connected to the server.
Terminated(client)
- Aww, someone disconnected...
Since we registered for Terminated
messages from clients when they connected, we need to handle them. In this case the server simply notifies the other clients that a client disconnected, then removes the client from the client list.
Next up, the client actor
class Client(val username: String, server: ActorRef) extends Actor {
server ! Connect(username)
def receive = {
case NewMsg(from, msg) => {
println(f"[$username%s's client] - $from%s: $msg%s")
}
case Send(msg) => server ! Broadcast(msg)
case Info(msg) => {
println(f"[$username%s's client] - $msg%s")
}
case Disconnect => {
self ! PoisonPill
}
}
}
There isn't much actor related to tell that hasn't been told in the server part. However, there are a few things worth mentioning.
On creation, the client sends a
Connect
message to the server.On
Disconnect
, the client sends aPoisonPill
message to itself to trigger the termination. ATerminated
message will then be published to DeathWatch, which will then notify our server actor.
Let the chatting begin!
To keep the chat a bit more natural, I’m using sbt console
to interact with the actors.
First, let's startup the console and get in a few dependencies.
$ sbt console
> import scamsg._
> import akka.actor._
Now, let's create our ActorSystem
.
> val system = ActorSystem("System")
This is the entry point for creating our actors. Actors created under the same ActorSystem
share several configurations and form a hierarchical group.
Creating an ActorSystem is quite expensive, so you shouldn't create them without having a good reason.
Next, let's create our server actor.
> val server = system.actorOf(Props[Server])
To create the actor, we use ActorSystem.actorOf
which creates a child under this context. Props
is an ActorRef
config object for creating new actors.
Ok, time to add a client!
> val c1 = system.actorOf(Props(new Client("Sam", server)))
> c1 ! Send("Hi, anyone here?")
[Sam's client] - Sam: Hi, anyone here?
Let's bring in someone for Sam to talk to.
> val c2 = system.actorOf(Props(new Client("Mia", server)))
[Sam's client] - Mia joined the chat
> val c3 = system.actorOf(Props(new Client("Luke", server)))
[Mia's client] - Luke joined the chat
[Sam's client] - Luke joined the chat
> c2 ! Send("Hello")
[Mia's client] - Mia: Hello
[Sam's client] - Mia: Hello
[Luke's client] - Mia: Hello
Great, let's now try to terminate Luke's client to see that the server actor will get notified.
> c3 ! Disconnect
[Mia's client] - Luke left the chat
[Sam's client] - Luke left the chat
That's it for now
That's it for this quick introduction on creating actors using Scala and Akka. I truly enjoy working with Akka, so I'll probably get back to the topic, covering more features like supervision strategies and distribution.
You can find the source code of the example on GitHub.